Pitfalls of Conditional Rendering and Refs in React
Tags: react, javascript
Published: 2019-11-30 23:40:37

Refs can be really useful in situations where you want to store or access some underlying data from components. Maybe you want to access the DOM node of a component or element?

While working with a component which both fetched some data as well as rendering the UI for that data, including handling the loading and error states. I wanted to use a ref to access the DOM node of an element to do some animation. This is where I ran into a pitfall of refs and conditionals.

Say, for example, that we have a component that fetches some data asynchronously—that is, handling something that happens at a later time—and shows some UI for that data. It could look something like this:

```js
function Post({ id }) {
  const { data } = useResource(`post/${id}`);

  return (
    <article>
      <h1>{data.title}</h1>
      <p>{data.body}</p>
    </article>
  );
}
```

Nice and neat! This looks really great. But now we want handle a special case. Let's say get the dimensions of the DOM node. This requires us to pass a ref to the component or element that we want to get the DOM node of. We can pass a ref to the `article` element to get its DOM node:

```js
function Post({ id }) {
  const containerRef = React.useRef(null);
  const { data } = useResource(`post/${id}`);

  return (
    <article ref={containerRef}>
      <h1>{data.title}</h1>
      <p>{data.body}</p>
    </article>
  );
}
```

To access the ref's value, we need to use a hook. It's important that we don't access refs in the body of the function component, [always inside the body of a hook](https://twitter.com/sebmarkbage/status/1199392290029965312). We can use `useEffect` so that we can get the ref when the component has rendered and set the ref's value to the DOM node of the `article` element.

If you don't know how refs and hooks are related and how refs are updated, I recommend reading [_Manipulating DOM Elements With React Hook useRef()_](https://dev.to/spukas/manipulating-dom-elements-with-react-hook-useref-446c) by Linus Spukas.

We access the ref and get the DOM node:

```js
function Post({ id }) {
  const containerRef = React.useRef(null);
  const { data } = useResource(`post/${id}`);

  useEffect(() => {
    const node = containerRef.current;
    console.log(node); // => HTMLArticleElement object
  }, []); 

  return (
    <article ref={containerRef}>
      <h1>{data.title}</h1>
      <p>{data.body}</p>
    </article>
  );
}
```

Later on, we decide that we also want to show a loading indicator when the post is loading the data. We can do this by adding a condition for when we want to render this loading indicator.

However, we know from the [_Rules of Hooks_](https://reactjs.org/docs/hooks-rules.html) that we can't call hooks conditionally. So we place the condition after all the `useEffect` and before the article:

```js
function Post({ id }) {
  const containerRef = React.useRef(null);
  const { data, loading } = useResource(`post/${id}`);

  useEffect(() => {
    const node = containerRef.current;
    console.log(node);
  }, []);

  if (loading) {
    return <Loading />
  }

  return (
    <article ref={containerRef}>
      <h1>{data.title}</h1>
      <p>{data.body}</p>
    </article>
  );
}
```

We run the code and... wait... what happened to our ref? It returns `null` now! What happened?

It's actually quite straight forward; the component will render the `Loading` component first, because the value of `loading` is initially set to `true` as we are loading data. When the data has loaded—successfully or not—it will set the loading to `false` and render our article.

However, this also means that when the `Post` component is committed for rendering, it first resolves the ref and then runs out `useEffect` hook. Since the `Loading` component doesn't have any ref set to it, React will not set the value of the ref. And since the hook is only run once—because we didn't pass any dependencies to it—it will not set the ref when our component finally renders the `article` element with the ref set to it.

There are multiple ways we can solve this. One way, which can be perfectly legitimate in some cases, would be to move the loading state inside a parent element and pass the `ref` to the parent element, like so:

```js
function Post({ id }) {
  const containerRef = React.useRef(null);
  const { data, loading } = useResource(`post/${id}`);

  useEffect(() => {
    const node = containerRef.current;
    console.log(node);
  }, []);

  return (
    <div ref={containerRef}>
      { loading ? <Loading /> : (
        <article>
          <h1>{data.title}</h1>
          <p>{data.body}</p>
        </article>
      ) }
    </div>
  );
}
```

This way we can be both schematically correct and get the ref again. This solved our problem And gives us the ref to another element.

But we can't access the `article` element directly and it adds extraneous divs to our schematics. We could move the logic _inside_ the `article` element instead:

```js
function Post({ id }) {
  const containerRef = React.useRef(null);
  const { data, loading } = useResource(`post/${id}`);

  useEffect(() => {
    const node = containerRef.current;
    console.log(node);
  }, []);

  return (
    <article ref={containerRef}>
      { loading ? <Loading /> : (
        <>
          <h1>{data.title}</h1>
          <p>{data.body}</p>
        </>
      ) }
    </div>
  );
}
```

It works! But what if we wanted to get the contents of the element? We can use `innerHTML` on the ref to try and get the contents:

```js
function Post({ id }) {
  const containerRef = React.useRef(null);
  const { data, loading } = useResource(`post/${id}`);

  useEffect(() => {
    const node = containerRef.current;
    console.log(node.innerHTML); // => [element of Loading]
  }, []);

  return (
    <article ref={containerRef}>
      { loading ? <Loading /> : (
        <>
          <h1>{data.title}</h1>
          <p>{data.body}</p>
        </>
      ) }
    </div>
  );
}
```

This will give us the element that the `Loading` component renders. We can't get the contents of the article without updating our component, either forcefully or removing dependencies from the hook.

Is there a way we can solve this? Absolutely!

## Lift me up

Since we're waiting for the data to be loaded before rendering the article, we can split that logic out to its own component. There's a well-known pattern called the [Container pattern](https://reactpatterns.com/#container-component) that can help us with this kind of separation.

Containers can be whatever you'd like. Often times they are entire screens or pages. Other times they're just concerned about preparing the data and return a presentational component. The important thing is just that we can separate the concerns between handling state or data and declaring our UI. Let's stick with the latter for the sake of simplicity.

We declare a Container component and move the logic for data fetching and handling loading state into it:

```js
function PostContainer({ id }) {
  const { data, loading } = useResource(`post/${id}`);

  if (loading) {
    return <Loading />
  }

  return <Post post={data} />;
}
```

We also change the props of the `Post` component to just accept the data through a `post` prop. This way, we can render the data for the post:

```js
function Post({ post }) {
  const containerRef = React.useRef(null);

  useEffect(() => {
    const node = containerRef.current;
    console.log(node);
  }, []);

  return (
    <article ref={containerRef}>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}
```

Now it works again! And we get our loading state. Our code also looks a lot nicer for handling the conditions.

We could handle other states here as well and the `Post` component will always be able to get the ref as it is rendered when everything is ready. With this it gives us a clear separation of data and UI, as well as solves our problem with the ref.

## Conclusion

This way of splitting up components, makes it quite simple to think about and helps to avoid some pitfalls that you may run into when working with conditionals.

The Container pattern also applies to class components, since they have the similar constraints for rendering components and handling refs when using lifecycle methods.