Search code examples
reactjsreact-hooksrecoiljs

Create and Read State for thousands of items using Recoil


I've just started using Recoil on a new project and I'm not sure if there is a better way to accomplish this.

My app is an interface to basically edit a JSON file containing an array of objects. It reads the file in, groups the objects based on a specific property into tabs, and then a user can navigate the tabs, see the few hundred values per tab, make changes and then save the changes.

I'm using recoil because it allows me to access the state of each input from anywhere in my app, which makes saving much easier - in theory...

In order to generate State for each object in the JSON file, I've created an component that returns null and I map over the initial array, create the component, which creates Recoil state using an AtomFamily, and then also saves the ID to another piece of Recoil state so I can keep a list of everything.

Question 1 Is these a better way to do this? The null component doesn't feel right, but storing the whole array in a single piece of state causes a re-render of everything on every keypress.

To Save the data, I have a button which calls a function. That function just needs to get the ID's, loop through them, get the state of each one, and push them into an Array. I've done this with a Selector too, but the issue is that I can't call getRecoilValue from a function because of the Rules of Hooks - but if I make the value available to the parent component, it again slows everything right down.

Question 2 I'm pretty sure I'm missing the right way to think about storing state and using hooks, but I haven't found any samples for this particular use case - needing to generate the state up front, and then accessing it all again on Save. Any guidance?


Solution

  • Question 1

    Get accustomed to null-rendering components, you almost can't avoid them with Recoil and, more in general, this hooks-first React world 😉

    About the useRecoilValue inside a function: you're right, you should leverage useRecoilCallback for that kind of task. With useRecoilCallback you have a central point where you can get and set whatever you want at once. Take a look at this working CodeSandbox where I tried to replicate (the most minimal way) your use-case. The SaveData component (a dedicated component is not necessary, you could just expose the Recoil callback without creating an ad-hoc component) is the following

    const SaveData = () => {
      const saveData = useRecoilCallback(({ snapshot }) => async () => {
        const ids = await snapshot.getPromise(carIds);
        for (const carId of ids) {
          const car = await snapshot.getPromise(cars(carId));
          const carIndex = db.findIndex(({ id }) => id === carId);
          db[carIndex] = car;
        }
        console.log("Data saved, new `db` is");
        console.log(JSON.stringify(db, null, 2));
      });
    
      return <button onClick={saveData}>Save data</button>;
    };
    

    as you can see:

    • it retrieves all the ids through const ids = await snapshot.getPromise(carIds);

    • it uses the ids to retrieve all the cars from the atom family const car = await snapshot.getPromise(cars(carId));

    All of that in a central point, without hooks and without subscribing the component to atoms updates.

    Question 2

    There are a few approaches for your use case:

    • creating empty atoms when the app starts, updating them, and saving them in the end. It's what my CodeSandbox does

    • doing the same but initializing the atoms through RecoilRoot' initialState prop

    • being updated by Recoil about every atom change. This is possible with useRecoilTransactionObserver but please, note that it's currently marked as unstable. A new way to do the same will be available soon (I guess) but at the moment it's the only solution

    The latter is the "smarter" approach but it really depends on your use case, it's up to you to think if you really want to update the JSON at every atom' update 😉

    I hope it helps, let me know if I missed something 😊