Patterns in React (wrapper)

442 views Asked by At

Good day. I'm building a tree of components and want to use functions of root component in other components of tree. I throw function reference through all tree. Also I use the object if me need get value from the function in not root componet. Can you help me? Can you show me how to do this as HOC ? If it will be not so hard for you show examples on my code.

import React from 'react';

class Page extends React.Component{

    Answer = {
        value : ''
    }

    doSomething(){

        console.log(this.Answer.value);
        console.log('Ready!');
    }
    
    render(){
        return(
            <div>
                <div>
                    <Body 
                        ParentFunc={()=>this.doSomething()} 
                        ParentParameters={this.Answer}
                    />
                </div>
            </div>
        )
    }
}

export default Page

class Body extends React.Component{
    render(){

        const{
            ParentFunc,
            ParentParameters
        } = this.props
        
        return(
            <div>
                <div>
                    <SomeComponent 
                        ParentFunc={()=>ParentFunc()}
                        ParentParameters={ParentParameters}
                    />
                </div>
            </div>
        )
    }
}

class SomeComponent extends React.Component{

    getAnswer(){
        
        const{
            ParentFunc,
            ParentParameters
        } = this.props

        ParentParameters.value = 'Some text'
        
        ParentFunc()
    }
    
    render(){
        
        return(
            <div onClick={()=>this.getAnswer()}>
                We can?
            </div>
        )
    }
}


2

There are 2 answers

0
Drew Reese On BEST ANSWER

I don't believe a Higher Order Component alone will solve your basic issue of prop drilling. A React Context would be a better fit for providing values and functions generally to "want to use functions of root component in other components of tree".

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

In a typical React application, data is passed top-down (parent to child) via props, but such usage can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree.

Start by creating your Context and Provider component:

const QnAContext = React.createContext({
  answer: {
    value: ""
  },
  doSomething: () => {}
});

const QnAProvider = ({ children }) => {
  const answer = {
    value: ""
  };

  const doSomething = () => {
    console.log(answer.value);
    console.log("Ready!");
  };

  return (
    <QnAContext.Provider value={{ answer, doSomething }}>
      {children}
    </QnAContext.Provider>
  );
};

Render QnAProvider in your app somewhere wrapping the React subtree you want to have access to the values being provided:

<QnAProvider>
  <Page />
</QnAProvider>

Consuming the Context:

  • Class-based components consume contexts via the render props pattern.

     <QnAContext.Consumer>
       {({ answer, doSomething }) => (
         <SomeComponent doSomething={doSomething} answer={answer}>
           We can?
         </SomeComponent>
       )}
     </QnAContext.Consumer>
    
  • Functional components can use the useContext React hook

     const SomeComponent = ({ children }) => {
       const { answer, doSomething } = useContext(QnAContext);
    
       getAnswer = () => {
         answer.value = "Some text";
    
         doSomething();
       };
    
       return <div onClick={this.getAnswer}>{children}</div>;
     };
    

Here is where using a Higher Order Component may become useful. You can abstract the QnAContext.Consumer render props pattern into a HOC:

const withQnAContext = (Component) => (props) => (
  <QnAContext.Consumer>
    {(value) => <Component {...props} {...value} />}
  </QnAContext.Consumer>
);

Then you can decorate components you want to have the context values injected into.

const DecoratedSomeComponent = withQnAContext(SomeComponent);

...

<DecoratedSomeComponent>We can with HOC?</DecoratedSomeComponent>

Note: The point of doing all this was to move the values and functions that were previously defined in Page into the Context, so they are no longer passed from Page though Body to SomeComponent (or any other children components).

Demo

Edit patterns-in-react-wrapper

Sandbox Code:

const QnAContext = React.createContext({
  answer: {
    value: ""
  },
  doSomething: () => {}
});

const QnAProvider = ({ children }) => {
  const answer = {
    value: ""
  };

  const doSomething = () => {
    console.log(answer.value);
    console.log("Ready!");
  };

  return (
    <QnAContext.Provider value={{ answer, doSomething }}>
      {children}
    </QnAContext.Provider>
  );
};

const withQnAContext = (Component) => (props) => (
  <QnAContext.Consumer>
    {(value) => <Component {...props} {...value} />}
  </QnAContext.Consumer>
);

class SomeComponent extends React.Component {
  getAnswer = () => {
    const { doSomething, answer } = this.props;

    answer.value = "Some text";

    doSomething();
  };

  render() {
    return (
      <button type="button" onClick={this.getAnswer}>
        {this.props.children}
      </button>
    );
  }
}

const DecoratedSomeComponent = withQnAContext(SomeComponent);

class Body extends React.Component {
  render() {
    return (
      <div>
        <div>
          <QnAContext.Consumer>
            {({ answer, doSomething }) => (
              <SomeComponent doSomething={doSomething} answer={answer}>
                We can?
              </SomeComponent>
            )}
          </QnAContext.Consumer>
        </div>
        <div>
          <DecoratedSomeComponent>We can with HOC?</DecoratedSomeComponent>
        </div>
      </div>
    );
  }
}

class Page extends React.Component {
  render() {
    return (
      <div>
        <div>
          <Body />
        </div>
      </div>
    );
  }
}

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>

      <QnAProvider>
        <Page />
      </QnAProvider>
    </div>
  );
}
0
Linda Paiste On

Based on your current code I am making the assumption that Body does not modify the values of ParentFunc and ParentParameters before passing them down to SomeComponent.

You have a hierarchy

<Page>
  <Body>
    <SomeComponent>
  </Body>
</Page>

and you want to pass props from Page to SomeComponent without going through Body.

You can do this using children

children is a special prop representing the JSX child elements of the component. We make it so that Body renders the children that it got through props:

class Body extends React.Component{
    render() {
        return(
            <div>
                <div>
                    {this.props.children}
                </div>
            </div>
        )
    }
}

We set that children prop by using a <SomeComponent/> element inside of the <Body>:

render() {
  return (
    <div>
      <div>
        <Body>
          <SomeComponent
            ParentFunc={() => this.doSomething()}
            ParentParameters={this.Answer}
          />
        </Body>
      </div>
    </div>
  );
}

Note that you cannot directly modify the value that you got from the parent, which is what you were doing with ParentParameters.value = 'Some text'. If you want to update the state of the parent then you need to do that through your callback function props. So your code should look something like this:

import React from "react";

class Body extends React.Component {
  render() {
    return (
      <div>
        <div>{this.props.children}</div>
      </div>
    );
  }
}

class SomeComponent extends React.Component {
  state = {
    showAnswer: false
  };

  onClick() {
    // update answer in parent
    this.props.setAnswer("Some text");
    // change state to reveal answer
    this.setState({ showAnswer: true });
  }

  render() {
    return (
      <div>
        {this.state.showAnswer && <div>Answer is: {this.props.answer}</div>}
        <div onClick={() => this.onClick()}>We can?</div>
      </div>
    );
  }
}

class Page extends React.Component {
  state = {
    value: ""
  };

  render() {
    return (
      <div>
        <div>
          <Body>
            <SomeComponent
              answer={this.state.value}
              setAnswer={(answer) => this.setState({ value: answer })}
            />
          </Body>
        </div>
      </div>
    );
  }
}

export default Page;