jrkt
jrkt

Reputation: 2715

ReactJS Accessing Props of Rendered Component

I am building a component that will be used for step-through processes such as :

enter image description here

This Workflow component takes an array of 'steps' as a prop and then it does the rest. Here is how it is being called in the image above :

let steps = [
{
    display: "Sign Up Form",
    component: SignupForm
},
{
   display: "Verify Phone",
   component: VerifyPhone
},
{
   display: "Use Case Survey",
   component: UseCase
},
{
   display: "User Profile",
   component: UserProfile
},
];

return (
    <Workflow
        steps={steps}
    />
);

The component field points to the component to be rendered in that step. For example the SignupForm component looks like this :

export default class SignupForm extends React.Component {
    ...
    render() {
        return (
            <div>
                <div className="page-header">
                    <h1>New User Sign Up Form</h1>
                    <p>Something here...</p>
                </div>

                <div className="form-group">
                    <input type="email" className="form-control" placeholder="Email address..." />
                    <small id="emailHelp" className="form-text text-muted">We'll never share your email with anyone else.</small>
                </div>
            </div>
        );
    }
}

The issue I'm facing is that in each step there needs to be a Next button to validate the information in that step and move to the next. I was going to just put that button inside the component of each step, but that makes it less user-friendly. When a user clicks 'Next', and everything is valid, that step should be collapsed and the next step should open up. However this means that my Workflow component needs to render this button.

So, I need my Workflow component to call the method of each step component to validate the information in the step and return a promise letting it know if it passed or failed (with any error message). How do I need to call this method? Here is where the Workflow component renders all the steps as <step.component {...this.props} />:

{
    this.state.steps.map((step, key) => {
        return (
            ...
                <Collapse isOpen={!step.collapsed}>
                    <step.component {...this.props} />
                    <Button color="primary"
                            onClick={() => this.validate(key)}>Next</Button>
                    <div className="invalid-value">
                        {step.error}
                    </div>
                </Collapse>
            ...
        );
    })
}

That renders the next button, as well as the onClick handler validate():

    validate(i) {
        let steps = _.cloneDeep(this.state.steps);
        let step = steps[i];

        step.component.handleNext().then(function () {
            ...
        }).catch((err) => {
            ...
        });
    }

Ideally, step.component.validate() would call the validate method inside that component that has already been rendered:

export default class SignupForm extends React.Component {
    ....

    validate() {
        return new Promise((resolve, reject) => {
            resolve();
        })
    }

    render() {
        ...
    }
}

.. which would have access to the state of that component. But, thats not how it works. How can I get this to work? I read a little about forwarding refs, but not exactly sure how that works. Any help is greatly appreciated!

Upvotes: 3

Views: 686

Answers (1)

Ross Allen
Ross Allen

Reputation: 44880

One approach is to apply the Observer pattern by making your form a Context Provider and making it provide a "register" function for registering Consumers. Your consumers would be each of the XXXForm components. They would all implement the same validate API, so the wrapping form could assume it could call validate on any of its registered components.

It could look something like the following:

const WorkflowContext = React.createContext({
  deregisterForm() {},
  registerForm() {},
});

export default class Workflow extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      forms: [],
    };
  }

  deregisterForm = (form) => {
    this.setState({
      forms: this.state.forms.slice().splice(
        this.state.forms.indexOf(forms), 1)
    });
  };

  registerForm = (form) => {
    this.setState({ forms: [ ...this.state.forms, form ] })
  };

  validate = () => {
    const validationPromises = this.state.forms.reduce(
      (promises, formComponent) => [...promises, formComponent.validate()]);
    Promise.all(validationPromises)
      .then(() => {
        // All validation Promises resolved, now do some work.
      })
      .catch(() => {
        // Some validation Promises rejected. Handle error.
      });
  };

  render() {
    return (
      <WorkflowContext.Provider
        value={{
          deregisterForm: this.deregisterForm,
          registerForm: this.registerForm,
        }}>
        {/* Render all of the steps like in your pasted code */}
        <button onClick={this.validate}>Next</button
      </WorkflowContext.Provider>
    );
  }
}

// Higher-order component for giving access to the Workflow's context
export function withWorkflow(Component) {
  return function ManagedForm(props) {
    return (
      <WorkflowContext.Consumer>
        {options =>
          <Component
            {...props}
            deregisterForm={options.deregisterForm}
            registerForm={options.registerForm}
          />
        }
      </WorkflowContext.Consumer>
    );
  }
}

SignupForm and any other form that needs to implement validation:

import { withWorkflow } from './Workflow';

class SignupForm extends React.Component {
  componentDidMount() {
    this.props.registerForm(this);
  }

  componentWillUnmount() {
    this.props.deregisterForm(this);
  }

  validate() {
    return new Promise((resolve, reject) => {
      resolve();
    })
  }

  render() {
    ...
  }
}

// Register each of your forms with the Workflow by using the
// higher-order component created above.
export default withWorkflow(SignupForm);

This pattern I originally found applied to React when reading react-form's source, and it works nicely.

Upvotes: 1

Related Questions