2

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?

Simon Park
  • 694
  • 1
  • 7
  • 18

2 Answers2

0

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.

snnsnn
  • 10,486
  • 4
  • 39
  • 44
-2

Utilize the useEffect hook. When the component is first loaded, the useEffect hook can be used to retrieve the items from the API and store them in a state variable. This way, when the component re-renders, it can access the stored items in the state variable instead of making a new API call.

export default function Home() {
  const [items, setItems] = useState<Item[]>([]);
  useEffect(() => {
    async function fetchData() {
      setItems(await fetchItemsThroughExpensiveAPI());
    }
    fetchData();
  }, []);

  return (
    <main>
      <For each={items}>
        {(item) => (
          <A href={`/item/${item.id}`}>{item.name}</A>
        )}
      </For>
    </main>
  );
}
  • This is not a react question. There is no useEffect in solid. Even if you use createEffect, it still does not make sense, since onMount is already an effect. – snnsnn Jan 20 '23 at 11:09
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Ahmed Sbai Jan 21 '23 at 21:35