Search code examples
frontendsolid-js

How to prevent re-rendering when switching the pages in SolidJS?


I'm struggling with the re-rendering issue in the SolidJS application. I have two routes, Home and Detail. A user can explore items in Home, and click the link on the item name to switch a page to Detail to check out detailed information.

export default function Home() {
  const [items, setItems] = createSignal<Item[]>([]);

  onMount(async () => {
    setItems(
      await fetchItemsThroughExpensiveAPI()
    );
  });

  return (
    <main>
      <For each={items()}>
        {(item) => (
          <A href={`/item/${item.id}`}>{item.name}</A>
        )}
      </For>
    </main>
  );
}
export default function Detail() {
  const params = useParams<{ id: string }>();
  
  return (
    <main>
      // Some detailed information for the item ...
    </main>
  );
}

At this point, the API(fetchItemsThroughExpensiveAPI) will be called back when the user returns to the Home from Detail. I'm expecting this it is caused by re-rendering. How do I prevent re-rendering Home whenever a user returns to Home from another page to avoid unnecessary API calls?


Solution

  • Use a resource to fetch the data outside the Home component. If you need to fetch the data once during application's life, cache it.

    https://www.solidjs.com/docs/latest/api#createresource

    Lets make it more clear. There are different patterns to render async data, data that resides in a remote location.

    1. Fetch as you render: In this pattern, data is fetched when the component mounts.

    ComponentA below uses this pattern. Whenever it is re-rendered, data will be re-fetched.

    1. Fetch then render: In this pattern, data is fetched outside the component, in one of its parent's scope. When component mounts it can use whatever is currently available, by whatever, I mean the request may not be resolved yet so state will be pending.

    Resource API is build to make use of this pattern.

    ComponentB below uses this pattern. Since data is fetched outside the component, re-rendering has no effect on it.

    import { Accessor, Component, createSignal, Match, Switch } from 'solid-js';
    import { render } from 'solid-js/web';
    
    interface State { status: 'pending' | 'resolved' | 'rejected', data?: any, error?: any };
    
    function getData(): Accessor<State> {
      const [state, setState] = createSignal<State>({ status: 'pending' });
      setTimeout(() => {
        setState({ status: 'resolved', data: { name: 'John Doe', age: 30 } });
      }, 1000);
      return state;
    };
    
    
    const ComponentA = () => {
      const state = getData();
      return (
        <Switch fallback={<div>Not Found</div>}>
          <Match when={state().status === 'pending'}>
            Loading...
          </Match>
          <Match when={state().status === 'resolved'}>
            {JSON.stringify(state().data)}
          </Match>
          <Match when={state().status === 'rejected'}>
            {JSON.stringify(state().error)}
          </Match>
        </Switch>
      );
    };
    
    const ComponentB: Component<{ state: Accessor<State> }> = (props) => {
      return (
        <Switch fallback={<div>Not Found</div>}>
          <Match when={props.state().status === 'pending'}>
            Loading...
          </Match>
          <Match when={props.state().status === 'resolved'}>
            {JSON.stringify(props.state().data)}
          </Match>
          <Match when={props.state().status === 'rejected'}>
            {JSON.stringify(props.state().error)}
          </Match>
        </Switch>
      );
    };
    
    const App = () => {
      const state = getData();
      const [show, setShow] = createSignal(false);
      const handleClick = () => setShow(prev => !prev);
      return (
        <div>
          {show() && (<ComponentA />)}
          {show() && (<ComponentB state={state} />)}
          <div><button onclick={handleClick}>Toggle Show Components</button></div>
        </div>
      )
    };
    
    render(() => <App />, document.body);
    

    Here you can see it in action: https://playground.solidjs.com/anonymous/32518df5-9840-48ea-bc03-87f26fecc0f4

    Here we simulated an async request using setTimeout. This is very crude implementation to prove a point. If you are going to fetch a remote resource, you should use the Resource API which provides several utilities like automatic re-fecthing when request parameters change.

    There are few issues with your implementation. First and foremost, async data in never guaranteed to be received, so you should handle failures.

    Since it takes some time to receive the remote data, you have to show the user some indicator of the ongoing request, like a loader.

    If API call is an expensive operation, you have to cache the result, rather than forcing the UI not to re-render. In this sense, your approach is very problematic.