Jordan
Jordan

Reputation: 1121

React TS - How to pass props from a parent component to a deeply nested child

Currently, I have created some different form elements. I'm trying to create them in a diverse way so I can piece them together to create different formats for a form.

Here are a few of my form elements:

// Field Component
interface IField extends ILabel {}

export const Field: React.FunctionComponent<IField> = props => {
  return (
    <div>
      <Label {...props} />
      {props.children}
    </div>
  );
};

// Label Component
interface ILabel {
  htmlFor: string;
  label: string;
  required?: boolean;
}

export const Label: React.FunctionComponent<ILabel> = props => {
  return (
    <label htmlFor={props.htmlFor}>
      {props.label}
      // Some required icon would go where I've added the <span />.
      {props.required && <span />}
    </label>
  );
};

// Input Wrapper Component
export const InputWrapper: React.FunctionComponent = props => {
  return <div>{props.children}</div>;
};

// Input Component
interface IInput {
  type: string;
  id: string;
  name: string;
  value?: string;
  placeholder?: string;
  required?: boolean;
}

export const Input: React.FunctionComponent<IInput> = props => {
  return (
    <input
      type={props.type}
      id={props.id}
      name={props.name}
      value={props.value}
      placeholder={props.placeholder}
      required={props.required}
    />
  );
};

And here's how I implement the component:

<Field htmlFor="name" label="Name:" required>
  <InputWrapper>
    <Input
      id="name"
      type="text"
      name="name"
      placeholder="Enter your name..."
    />
  </InputWrapper>
</Field>

I would like to be able to set the required prop on the Field component and it will also get passed down to my Input component, no matter how deeply nested it becomes. How is this possible?

I'm also providing a CodeSandBox demo.

Thanks for any help in advance!

Upvotes: 3

Views: 1834

Answers (1)

j3ff
j3ff

Reputation: 6089

When using props.children and that it is impossible to know the exact hierarchy of the nested components, or that it may vary for different use-cases, it can be hard to try to pass the props from parent to children.

Context provide a way to share data and react on changes, for a tree of components.

Your codesandbox adapted to the explained solution
https://codesandbox.io/s/react-stackoverflow-60241936-fvkdo

In your case, you would need to create a Context for your Field component to "share" the required field and define a default value.

interface IFieldContext {
  required?: boolean;
}

export const FieldContext = React.createContext<IFieldContext>({
  required: false // default value when the prop is not provided
});

Then using FieldContext.Provider in the Field component you can assign the "shared" values for the nested components.

export const Field: React.FunctionComponent<IField> = props => {
  return (
    <FieldContext.Provider value={{ required: props.required }}>
      <Label {...props} />
      {props.children}
    </FieldContext.Provider>
  );
};

Finally, in the Input component, use FieldContext.Consumer to access the "shared" values and retrieve the required prop assigned on the Field component. You will then be able to remove the required field from the IInput interface since it now comes from the Context and not from props anymore.

interface IInput {
  type: string;
  id: string;
  name: string;
  value?: string;
  placeholder?: string;
}

export const Input: React.FunctionComponent<IInput> = props => {
  return (
    <FieldContext.Consumer>
      {context => (
        <input
          type={props.type}
          id={props.id}
          name={props.name}
          value={props.value}
          placeholder={props.placeholder}
          required={context.required} // access context prop
        />
      )}
    </FieldContext.Consumer>
  );
};

And "voila", you can use the required prop on the Field component and it will be applied on your nested Input component, no matter how deep the Input is nested... fancy stuff 😄

export const App: React.FunctionComponent = () => {
  return (
    <Field htmlFor="name" label="Name:" required>
      <InputWrapper>
        <Input
          id="name"
          type="text"
          name="name"
          placeholder="Enter your name..."
        />
      </InputWrapper>
    </Field>
  );
};

Upvotes: 1

Related Questions