0
votes

I'm exploring react/redux and I'm trying to work out the best way to implement a multi-step wizard component where the Next "step" (i.e. component) is dynamically determined at runtime based on server-side workflow rules. I looked at redux-form and other wizard-like components but only saw hard-coded steps like this example from redux-form (props omitted for brevity):

return (<div>
    {page === 1 && <WizardFormFirstPage />}
    {page === 2 && <WizardFormSecondPage/>}
    {page === 3 && <WizardFormThirdPage />}
  </div>
)

My first (naive) experiment toward a dynamic solution involved the following function for rendering a given component into a host div (I'm using the term "host" instead of "container" to avoid ambiguity with Dan Abramov's concept of "container components"):

showComponent(componentName) {
  let renderFunc = React.createElement(components[componentName], null);
  render(renderFunc, this.refs['hostDiv']);
}

This works with [simple] presentational components - yay... Of course, this is useless because the newly rendered component doesn't get any props.

My next experiment was to see if this would work with redux-connected container components. As you surely can guess, I failed miserably; if componentName refers to a container component, I get:

Uncaught Invariant Violation: Could not find "store" in either the context or props of "Connect(Borrowers)". Either wrap the root component in a , or explicitly pass "store" as a prop to "Connect(Borrowers)".

https://github.com/reactjs/react-redux/blob/ea53d6fb076359a864a1d543568d951d4b3eab3d/src/components/connect.js#L86

The error message is telling me how stupid I am for passing null as the second argument to createElement(). So here's the next iteration:

showComponent(componentName) {
  const { store } = this.context;
  let renderFunc = React.createElement(components[componentName], { store });
  render(renderFunc, this.refs['hostDiv']);
}. 

This makes redux happy and seems to work. However, I don't feel great about using React's experimental context feature. This should typically be reserved for libraries.


So...

Q1. Is there a recommended way to dynamically render a component and wire it up to the redux store in a seamless manner, as if it were part of the static component hierarchy (including preserving time travel debugging and not screwing up React's VDOM reconciliation process)?

Q2. Does anyone foresee any problems with my approach (barring the usage of context)?

Q3. What are the performance implications of my approach, with regards to VDOM reconciliation?

Note: I tagged "react-router" because I'm open to a router-based approach as well. Keep in mind, I won't know my routes until runtime. I know react-router supports dynamic routing with code splitting, but I'm not sure I can inject/replace new routes at runtime.

Edit: I've stricken the above because I think any router solution would be encapsulated in a HOC and the componentName would simply be passed down via props to the actual presentation component in question.

1
For Q1, have you looked at react-router's routing? You can have dynamic params in url to match.Yuya
@Kujira Hi, I have looked at react-router's params and I think that would certainly work for relatively simple UI. Admittedly, I'd have to explore this further to prove it's suitable for a complex app with potentially multiple instances of the task wizard within multiple levels of nested tabs.Tony D

1 Answers

0
votes

It seems like react-router is the better case for what you're trying to do here. I'm not sure why you would think it's only for a simple UI. From what I'm gathering you are describing exactly what react-router will do for you...

<Router>
    <Route to="/" component={Main}>
        <Route to="1" component={WizardFormFirstPage} />
        <Route to="2" component={WizardFormSecondPage} />
        <Route to="3" component={WizardFormThirdPage}>
            <Route to=":someId" component={WizardFormThirdPageWithId} />
        </Route>
    <Route>
</Router>

This would look something like this in your url to get to these pages...

http://localhost/1
http://localhost/2
http://localhost/3

and just for fun, I added a child to one of the routes so you could see that you can get a param passed to the WizardFormThirdPageWithId. That url would look something like http://localhost/3/2345 and the value of this.props.params.someId in the WizardFormThirdPageWithId would equal 2345.

You don't necessarily need the Main component and the parent route, but you'd have to either do a redirect in react router, or just render one of your pages for the initial '/'.