Wednesday, May 18, 2016

Adding a "ref" to a Dynamically Inserted Child Component in React

I was working on a wizard type multi-step component for a React Application I was building (the component is called react-stepzilla in case you are interested in it)

It basically let's you throw a bunch of Child Components at it and it will take the user through those Components in a wizard type journey.

So basically it lets you build something like this.


Figure 1: react-stepzilla component look


The obvious use case for this would be to collect information from the user and validate that information as you go.

In this example, you initialise, declare and render the Component in your App (something) like this:

import Step1 from './Step1';

import Step2 from './Step2';

import StepZilla from 'react-stepzilla'

...

const steps =

    [

      {name: 'Welcome', component: <Step1 />},

      {name: 'Personals', component: <Step2 />},

      {name: 'Emergency', component: <Step3 />},

      {name: 'Medical', component: <Step4 />},

      {name: 'Review', component: <Step5 />}

    ]

...

render() {

    return (

        <div className='step-progress'>

            <StepZilla steps={steps}/>

         </div>

    )

}

and the outcome would look like figure 1.

As I was building this I hit a roadblock. Some of my Child "Step" Components (e.g. Step2) was a form that needed to be validated before we can move on to the next Step (Step3).

My Step2 Component exposed a public method called "isValidated()" which runs the entire form through validation and then returns a true/false indicator on validation state.

But Step2 was a Child Component to my StepZilla Parent Component and I didn't have access to any of it's methods via 'this.props.children' which is how I thought you should (I am fairly new to React I should add) but then I read that this is not possible (read https://facebook.github.io/react/tips/children-undefined.html)

I needed someway to reach into my Child Step2 Component and invoke isValidated() before continuing.

Upon further reading "refs" seemed to be the facility given by React to reach into Child Components (read https://facebook.github.io/react/docs/more-about-refs.html) and this seemed like the way I should do this.

But "refs" can only be declared to a Child Component in the render() method of the Parent Component. In the StepZilla Parent I was passing in a array of Child Components which was then injected into the Parent Component (something) like this:

import React, { Component, PropTypes } from 'react';


export default class StepZilla extends Component {

    ...

      _next() {

            // move to next step (if current step needs to be validated then do that first!)

            if (typeof this.refs.activeComponent.isValidated == 'undefined' ||

                this.refs.activeComponent.isValidated()) {

          

                    this._setNavState(this.state.compState + 1);

            }

      }

    ...

    render() {

        return (

            {this.props.steps[this.state.compState].component}

        )

    }

    ...

}

So I needed someway to intercept this injection and append a "ref" to the component so I can then reflect of it's validation state later on (in _next() method).

After much digging around I came up with a solution. Which is basically to clone the Child React Element and then append a "ref" to it. This was how I made it work.
export default class StepZilla extends Component {

    ...

    render() {

        const compToRender = React.cloneElement(this.props.steps[this.state.compState].component, {

            ref: 'activeComponent'

            }

        });

    return (

        {compToRender}

      )

    }

}

And this works in my situation!

A few notes relating to this design approach:
  1. Ideally in React you should not "reach into" Child and Parent Components as this goes back to a imperative modal of coding which React is trying to take us out of. But in some situations you cant avoid this. I did think of doing the above using "events" but it didn't make sense to me to overcomplicate this use case.
  2. I believe React.cloneElement does a "Shallow Clone" so it may have some adverse effects (which I still have not encountered but we cant rule it out)

Hope this helps.

Happy React Hacking!

No comments:

Post a Comment

Fork me on GitHub