Developer's blog

Go to Notes

Code Reuse with React Hooks

One of the most challenging problems for frontend developers is the reuse of code. It’s especially important for big applications. Traditional approaches with higher-order components and render-props have advantages and disadvantages. In some cases, it can be inconvenient to use them. React 16.8 introduced a new feature called React Hooks. Let’s take a look at it and try to figure out how does it work and how does it solve the problem of code reuse.

Problem of code reuse

During the software development process of frontend project, every developer meets code reuse problem sooner or later. This code can be stateless or stateful. A typical example of stateless shared code is date formatting function. It can be used like this:

import { formatDate } from 'utils';

function Comment(props) {
  return (
    <div className="comment">
      <div className="createdAt">{formatDate(props.createdAt)}</div>
      <p>{props.text}</p>
    </div>
  );
}

It’s easy to use stateless shared code because you don’t need to think about the state, so it can be moved into pure functions.

A More difficult situation is when you need some state. E.g. several components need the current user name and array of comments. The logic of data fetching is the same and we want to share it between all components.

Let’s compare different approaches to solving this problem.

Render props

Render props is a technique for sharing the code between React components using a prop whose value is a function. Such an approach makes it possible to separate logic and rendering into two different components.

class UserProvider extends React.Component {
  state = { user: null };

  async componentDidMount() {
    const user = await fetchUser();
    this.setState({ user });
  }

  render() {
    return this.props.render(this.state.user);
  }
}

function App(user) {
  return (
    <p>
      Current user: {user ? user.name : 'anonymous'}
    </p>
  );
}

ReactDOM.render(<UserProvider render={App} />, document.getElementById('app'));

In the example above user fetching extracted into separate UserProvider component. This component takes a function as a render prop that returns a React element and calls it instead of implementing its own render logic, so different components can reuse UserProvider.

If some component needs two different render props components it can be implemented like this:

class UserProvider extends React.Component {
  state = { user: null };

  async componentDidMount() {
    const user = await fetchUser();
    this.setState({ user });
  }

  render() {
    return this.props.render(this.state.user);
  }
}

class CommentsProvider extends React.Component {
  state = { comments: [] };

  async componentDidMount() {
    const comments = await fetchComments();
    this.setState({ comments });
  }

  render() {
    return this.props.render(this.state.comments);
  }
}

function App(user, comments) {
  return (
    <>
      <p>Current user: {user ? user.name : "anonymous"}</p>
      <div className="comments">
        {comments.map(c => (
          <div className="comment">{c.text}</div>)
        )}
      </div>
    </>
  );
}

ReactDOM.render(
  <UserProvider
    render={user => (
      <CommentsProvider render={comments => (
        App(user, comments)
      )} />;
    )}
  />,
  document.getElementById("app")
);

As you can see, this code looks a bit dirty.

Higher-Order Components

Higher-order components (HOC) is a function that takes a component and returns a new component. Such a technique can be used for extracting logic into a wrapper-component and reuse it later in several places.

function withUser(WrappedComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = { user: null };
    }

    async componentDidMount() {
      const user = await fetchUser();
      this.setState({ user });
    }

    render() {
      return (
        <WrappedComponent {...this.props} user={this.state.user} />
      );
    }
  }
}

function App({ user }) {
  return (
    <p>
      Current user: {user ? user.name : 'anonymous'}
    </p>
  );
}

const WrappedApp = withUser(App);

ReactDOM.render(<WrappedApp />, document.getElementById('app'));

In the example above user fetching extracted into separate component returned by withUser function. This component wraps the original App component and provides user property for it.

Any component can be wrapped by several HOCs to connect different logic:

function withUser(WrappedComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = { user: null };
    }

    async componentDidMount() {
      const user = await fetchUser();
      this.setState({ user });
    }

    render() {
      return (
        <WrappedComponent {...this.props} user={this.state.user} />
      );
    }
  }
}

function withComments(WrappedComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = { comments: [] };
    }

    async componentDidMount() {
      const comments = await fetchComments();
      this.setState({ comments });
    }

    render() {
      return (
        <WrappedComponent {...this.props} comments={this.state.comments} />
      );
    }
  }
}

function App({ user, comments }) {
  return (
    <>
      <p>Current user: {user ? user.name : "anonymous"}</p>
      <div className="comments">
        {comments.map(c => (
          <div className="comment">{c.text}</div>)
        )}
      </div>
    </>
  );
}

const WrappedApp = withComments(withUser(App));

ReactDOM.render(<WrappedApp />, document.getElementById('app'));

I think such chain withCommens(withUser(App)) looks much better than render props variant.

All wrapper-components are displayed in React Debugger:

Wrappers

For the long chains, it can be difficult to inspect and debug such tree but I think if an application contains such chains maybe its structure needs to be redesigned.

React Hooks

Before React 16.8 there were two types of components: stateless functional components and stateful class components. In React 16.8 Hooks were introduced. As it is said in the official documentation:

If you write a function component and realize you need to add some state to it, previously you had to convert it to a class. Now you can use a Hook inside the existing function component.

So the border between stateful and stateless components is blurring.

function useUser() {
  const [user, setUser] = useState(null);

  useEffect(async () => {
    const u = await fetchUser();
    setUser(u);
  }, []);

  return user;
}

function App() {
  const user = useUser();

  return (
    <p>
      Current user: {user ? user.name : 'anonymous'}
    </p>
  );
}

ReactDOM.render(<App />, document.getElementById('app'));

Before React Hooks it was a rule that functional components do not have any state but now it’s not true. React provides some sort of external context for every functional component and React Hooks use this context to store state and perform some effects like componentDidMount hook does for class components. I suggest reading react fiber architecture description to understand better how React Hooks is implemented.

In the example above custom hook useUser was introduced. It encapsulates user state and makes it possible to use it in App functional component. Several custom hooks can be used in one functional component:

function useUser() {
  const [user, setUser] = useState(null);

  useEffect(async () => {
    const u = await fetchUser();
    setUser(u);
  }, []);

  return user;
}

function useComments() {
  const [comments, setComments] = useState([]);

  useEffect(async () => {
    const c = await fetchComments();
    setComments(c);
  }, []);

  return user;
}

function App() {
  const user = useUser();
  const comments = useUser();

  return (
    <>
      <p>Current user: {user ? user.name : "anonymous"}</p>
      <div className="comments">
        {comments.map(c => (
          <div className="comment">{c.text}</div>)
        )}
      </div>
    </>
  );
}

ReactDOM.render(<App />, document.getElementById('app'));

The example with hooks requires less code and it seems that it is easier to read but there are some disadvantages to this approach.

First of all, it’s more difficult to understand how all this stuff works. When you use class components internal data is stored in this.state variable and the only source of external data is props. It’s easy to understand such code works and easy to debug it because all your internal data encapsulated in your instance. When you use hooks you have to know that your function is being executed in some special React context and Hooks can store and extract data using some sort of magic, you need to remember Rules of Hooks to write code without errors.

Also, it’s more difficult to test your code with React Hooks. Assume you want to check rendering logic. In case of render props or HOC you just provide some attributes for your presentational component and check the result:

function App({ user, comments }) {
  return (
    <>
      <p>Current user: {user ? user.name : "anonymous"}</p>
      <div className="comments">
        {comments.map(c => (
          <div className="comment">{c.text}</div>)
        )}
      </div>
    </>
  );
}

it('Renders "anonymous" if user prop is null', () => {
  const app = shallow(<App user={null} comments={[]} />);
  expect(app.text()).to.contain('anonymous');
});

In case of React Hooks, you have to mock fetchUser and fetchComments function somehow. Sometimes it can tricky and moreover you check not only rendering logic but custom hooks logic too.

Conclusion

I think React Hooks is a tricky feature which has advantages and disadvantages. Sometimes it can be useful and sometimes it can make your code harder to understand, so think twice before integrate it into your project.