Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-render when one item in collection is updated. #102

Open
gwen1230 opened this issue Dec 22, 2022 · 6 comments
Open

Re-render when one item in collection is updated. #102

gwen1230 opened this issue Dec 22, 2022 · 6 comments

Comments

@gwen1230
Copy link

Hello !
I use the hook useCollection with an Hypermedia API (HAL) and i have problems with ketting cache.

I have an endpoint who returns a JSON like

{
  _embedded: {
    items: [
      {{code: 'CODE_1}, links: {self: XXX}},
      {{code: 'CODE_2}, links: {self: XXX}}
    ]
  _links: {
    self: 'api/test?from=XXX&to=XXX'
  }
}

And i use useCollection like this :

  const [state, setState] = useState([]);
  const { items } = useCollection(
    res.follow('collection', {
      from: currentMonth.toISOString(),
      to: currentMonth.toISOString(),
    }),
    { rel: 'items', refreshOnStale: true }
  );

  useEffect(() => {
    async function populateData() {
      setState(await Promise.all(items.map((t) => t.get())));
    }
    populateData();
  }, [items]);

  return state.map((s) => <>s.code</>)

When I perform certain actions on another component, ketting cache of one item in the previous list is updated, but the component that call the useCollection doesn't refresh (and the s.code remains the same).
Did I misunderstand something?

@gwen1230
Copy link
Author

I have found a solution :

export function useResourceList<T>(state: ResourceLike<unknown>, rel = 'items') {
  const [internalState, setInternalState] = useState<State<T>[]>([]);
  const [loading, setLoading] = useState<boolean>(false);
  const { items, loading: loadingCollection } = useCollection<T>(state, {
    rel,
  });

  async function populateData() {
    setLoading(true);
    items.forEach((item, idx) => {
      item.on('update', (state: State<T>) => {
        const newState = [...internalState];
        newState.splice(idx, 1, state);
        setInternalState(newState);
      });
    });
    const newState = await Promise.all(items.map((t) => t.get()));
    setInternalState(newState);
    setLoading(false);
  }
  useEffect(() => {
    populateData();
  }, [items]);
  return { state: internalState, loading: loadingCollection || loading };
}

Any remarks ?

@gwen1230
Copy link
Author

It doesn't work as expected, internalState in the 'on' hook is always empty

@evert
Copy link
Contributor

evert commented Dec 24, 2022

Hi Gwen,

The normal way to solve this that you use useCollection, and for each item (in items) that you receive you create a new component that uses the useResource hook.

So to rewrite your original example:

function MyCollection() {

  const { items, loading } = useCollection(
    res.follow('collection', {
      from: currentMonth.toISOString(),
      to: currentMonth.toISOString(),
    }),
    // Note that 'items' instead of 'item' is less common, so you might prefer to
    // rename this rel.
    { rel: 'items', refreshOnStale: true }
  );

  if (loading) return 'Loading...';

  return items.map(item => <MyItem key={item.url} resource={item});

}

function MyItem(props: {resource: Resource}) {

  const { data, loading } = useResource(resource);
  if (loading) return 'Loading...';
  return <>data.code</>;
}

The reason the event for changing resources is not changing the top-level, is because changes in the collection do not extend to 'embedded items'. Changes in the event only really apply to propertes on the collection itself, or membership changes (adding or removing item from collection), but not the state of the members themselves.

I do think having a hook like useResourceList would be useful in the future though, so definitely open to considering that a feature request.

@gwen1230
Copy link
Author

gwen1230 commented Jan 3, 2023

Yes I thought of this solution but it may not apply in some cases.
For example, I have pages that must display the sum of a field present in each element of a list...
When one element changes, the sum must be refreshed.

I also use react-table, and similarly, if an item in the list changes, I need to be able to refresh it.

I have improved the proposed code in this way:

export function useResourceList<T>(state: ResourceLike<unknown>, rel = 'items') {
  const [internalState, setInternalState] = useState<State<T>[]>([]);
  const [loading, setLoading] = useState<boolean>(false);
  const { items, loading: loadingCollection } = useCollection<T>(state, {
    rel,
  });

  async function populateData() {
    setLoading(true);
    const newState = await Promise.all(items.map((t) => t.get()));
    setInternalState(newState);
    setLoading(false);
  }

  useEffect(() => {
    items.forEach((item, idx) => {
      item.on('update', (state: State<T>) => {
        const newState = [...internalState];
        newState.splice(idx, 1, state);
        setInternalState(newState);
      });
    });
  }, [internalState]);

  useEffect(() => {
    if (!loadingCollection) {
      populateData();
    }
  }, [items, loadingCollection]);
  return { state: internalState, loading: loadingCollection || loading };
}

@evert
Copy link
Contributor

evert commented Jan 4, 2023

When one element changes, the sum must be refreshed.

That does make sense. The server-side way to handle this, is if you did a PUT request on any of these, and the server returns a link header (must be a header) in response:

Link: </collection>; rel="invalidates"

This basically lets a server say: "We completed the PUT request, the client should also be aware that the cache for the parent collection is also stale.

If the server does this, Ketting will fire all the right events and re-renders.

Would that solve your problem?

@gwen1230
Copy link
Author

gwen1230 commented Jan 5, 2023

I have a similar mechanism to invalidate the cache but that's not what I want.
If I modify an element in a list of 10,000 elements, I don't want to refresh all the elements...

Having failed to get by on this case and on other cases with react-ketting, I used ketting by developing my own hooks to chain the calls...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants