Recoil: useRecoilState inside custom hooks behaves weirdly

Created on 28 Sep 2020  路  7Comments  路  Source: facebookexperimental/Recoil

Hi there, im really excited about using Recoil because it will solve many of my scenarios with a minimal implementation :)

But something that i thought was very intuitive doesn't work.

I have a global state which is a query composed of key:value pairs, and i need to read this in different places of the app, but the way i update this from data is similar and it's not as simple as setQuery(string), i need to do some processing to form this string before setting it.

So I thought, I'll create a custom hook that would get the recoil state and have a function to process the string and then set the new recoil state.

But for some reason when I do setQuery from the RecoilState, it produces updates in an irregular manner.

Inside this custom hook I have a useEffect listening to query changes, but the first time it "reacts" to it, the query value is the previous instead of the new one.

I tried using a useRef for the state to keep it updated somewhere else, but I get the same result.

here's a code example:

const customHook = () => {
  const [query, setQuery] = useRecoilState(queryState); //string
  const [deserialized, setDeserialized] = useState(); 

  const updateQuery = (key, value) => {
    const newQuery = `${query} ${key}:${value}`
    setQuery(newQuery)
  }

  useEffect(() => {
    const newDeserialized = // logic  to deserialize value using query recoil state, but it's the outdated value so i get wrong results
     setDerserialized(newDeserialized)
  }, [query])

return [deserialized, updateQuery]

Is this just never going to work? Or am doing something wrong here?

Thanks and kind regards

Most helpful comment

We use browser history from createBrowserHistory() and then store the location in an atom.

export const _location = atom({
  key: '_location',
  default: browserHistory.location,
});

We then mount a synchronization component right beneath our <RecoilRoot>:

export function HistoryRecoilSync() {
  const history = RoutingRepository.history(); // gets the browser history object
  const set = useSetRecoilState(_location);

  useEffect(() => {
    return history.listen((location, action) => {
      // action = 'PUSH' | 'POP' | 'REPLACE';
      set(location);
    });
  }, [history, set]);

  return <></>;
}

It doesn't render anything, it just listens to history changes.

Then in your toplevel component in which you have the recoil root:

export default function Startup() {
  return (
    <RecoilRoot>
      {/* this component is empty and it's only purpose is to sync external state with Recoil */}
      <HistoryRecoilSync />
      <YourAppComponentHere />
    </RecoilRoot>
  );
}

That way you have the location available in Recoil. 馃槃

All 7 comments

If I were you I would use a selector to get the deserialized data:

const deserialized = selector({
    key: 'deserializedQueryState',
    get: ({ get }) => {
        const query = get(queryState);
        const deserialized = deserialize(query); // your deserialize logic here

        return deserialized;
    },
    set: ({ get, set }, { key, value }) => {
        const query = get(queryState);
        set(queryState, `${query} ${key}:${value}`);            
    }
})

And then you can use the selector instead of a custom hook with useRecoilState. This way you also ensure that any place that uses that query will get updated value.

If I were you I would use a selector to get the deserialized data:

const deserialized = selector({
  key: 'deserializedQueryState',
  get: ({ get }) => {
      const query = get(queryState);
      const deserialized = deserialize(query); // your deserialize logic here

      return deserialized;
  },
  set: ({ get, set }, { key, value }) => {
      const query = get(queryState);
      set(queryState, `${query} ${key}:${value}`);            
  }
})

And then you can use the selector instead of a custom hook with useRecoilState. This way you also ensure that any place that uses that query will get updated value.

That looks nice, i didn't know the selector also has a set prop, it doesn't show in the start guide :(

I'll give it a try, thanks!

Yeah, I always go straight to the api ref, it seems to have more examples and information. 馃槃
Link: https://recoiljs.org/docs/api-reference/core/selector#writeable-selectors

Yeah, I always go straight to the api ref, it seems to have more examples and information. 馃槃
Link: https://recoiljs.org/docs/api-reference/core/selector#writeable-selectors

So, this is taking quite a nice shape thanks to your suggestion :)

But i have one more question if it's ok.

I have not the situation, that i need to push a new history entry (with the new query values) when i do set, but since i can't use hooks inside the selector i can't easily use react-router's hooks to update the state.

Do you know of any recoil trick to "react" on state change (only once) and push history? or should i just give up on the router hooks and do window.history.push in the selector?

Best i can think of is moving the history management to a parent of all the components that implement the queryState, so when one of them changes this query the parent knows and reacts only once

We use browser history from createBrowserHistory() and then store the location in an atom.

export const _location = atom({
  key: '_location',
  default: browserHistory.location,
});

We then mount a synchronization component right beneath our <RecoilRoot>:

export function HistoryRecoilSync() {
  const history = RoutingRepository.history(); // gets the browser history object
  const set = useSetRecoilState(_location);

  useEffect(() => {
    return history.listen((location, action) => {
      // action = 'PUSH' | 'POP' | 'REPLACE';
      set(location);
    });
  }, [history, set]);

  return <></>;
}

It doesn't render anything, it just listens to history changes.

Then in your toplevel component in which you have the recoil root:

export default function Startup() {
  return (
    <RecoilRoot>
      {/* this component is empty and it's only purpose is to sync external state with Recoil */}
      <HistoryRecoilSync />
      <YourAppComponentHere />
    </RecoilRoot>
  );
}

That way you have the location available in Recoil. 馃槃

That's smart :)
I think that my solution will work for now since i only need history in 2 components that are wrapped by this parent, but I'll bookmark your solution because i think i will need something like this later on.

Thanks for the help!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Etherum7 picture Etherum7  路  3Comments

polemius picture polemius  路  3Comments

jamiebuilds picture jamiebuilds  路  3Comments

pesterhazy picture pesterhazy  路  4Comments

art1373 picture art1373  路  4Comments