Home > Mobile >  Dynamic atom keys in Recoil
Dynamic atom keys in Recoil

Time:03-19

I'm trying to make a dynamic form where the form input fields is rendered from data returned by an API.

Since atom needs to have a unique key, I tried wrapping it inside a function, but every time I update the field value or the component re-mounts (try changing tabs), I get a warning saying:

""

I made a small running example here https://codesandbox.io/s/zealous-night-e0h4jt?file=/src/App.tsx (same code as below):

import React, { useEffect, useState } from "react";
import { atom, RecoilRoot, useRecoilState } from "recoil";
import "./styles.css";

const textState = (key: string, defaultValue: string = "") =>
  atom({
    key,
    default: defaultValue
  });

const TextInput = ({ id, defaultValue }: any) => {
  const [text, setText] = useRecoilState(textState(id, defaultValue));

  const onChange = (event: any) => {
    setText(event.target.value);
  };

  useEffect(() => {
    return () => console.log("TextInput unmount");
  }, []);

  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}
    </div>
  );
};

export default function App() {
  const [tabIndex, setTabIndex] = useState(0);

  // This would normally be a fetch request made by graphql or inside useEffect
  const fields = [
    { id: "foo", type: "text", value: "bar" },
    { id: "hello", type: "text", value: "world" }
  ];

  return (
    <div className="App">
      <RecoilRoot>
        <form>
          <button type="button" onClick={() => setTabIndex(0)}>
            Tab 1
          </button>
          <button type="button" onClick={() => setTabIndex(1)}>
            Tab 2
          </button>

          {tabIndex === 0 ? (
            <div>
              <h1>Fields</h1>
              {fields.map((field) => {
                if (field.type === "text") {
                  return (
                    <TextInput
                      key={field.id}
                      id={field.id}
                      defaultValue={field.value}
                    />
                  );
                }
              })}
            </div>
          ) : (
            <div>
              <h1>Tab 2</h1>Just checking if state is persisted when TextInput
              is unmounted
            </div>
          )}
        </form>
      </RecoilRoot>
    </div>
  );
}

Is this even possible with recoil. I mean it seems to work but I can't ignore the warnings.

CodePudding user response:

I think the problem is from textState(id, defaultValue). Every time you trigger re-rendering for TextInput, that function will be called again to create a new atom with the same key.

To avoid that situation, you can create a global variable to track which atom added. For example

let atoms = {}
const textState = (key: string, defaultValue: string = "") => {
   //if the current key is not added, should add a new atom to `atoms`
   if(!atoms[key]) {
      atoms[key] = atom({
         key,
         default: defaultValue
      })
   }

   //reuse the existing atom which is added before with the same key
   return atoms[key];
}

CodePudding user response:

This answer shows how you can manually manage multiple instances of atoms using memoization.

However, if your defaultValue for each usage instance won't change, then Recoil already provides a utility which can take care of this creation and memoization for you: atomFamily. I'll quote some relevant info from the previous link (but read it all to understand fully):

... You could implement this yourself via a memoization pattern. But, Recoil provides this pattern for you with the atomFamily utility. An Atom Family represents a collection of atoms. When you call atomFamily it will return a function which provides the RecoilState atom based on the parameters you pass in.

The atomFamily essentially provides a map from the parameter to an atom. You only need to provide a single key for the atomFamily and it will generate a unique key for each underlying atom. These atom keys can be used for persistence, and so must be stable across application executions. The parameters may also be generated at different callsites and we want equivalent parameters to use the same underlying atom. Therefore, value-equality is used instead of reference-equality for atomFamily parameters. This imposes restrictions on the types which can be used for the parameter. atomFamily accepts primitive types, or arrays or objects which can contain arrays, objects, or primitive types.

Here's a working example showing how you can use your id and defaultValue (a unique combination of tuple values) as a parameter when using an instance of atomFamily state for each input:

TS Playground

body { font-family: sans-serif; }
input[type="text"] { font-size: 1rem; padding: 0.5rem; }
<div id="root"></div><script src="https://unpkg.com/[email protected]/umd/react.development.js"></script><script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script><script src="https://unpkg.com/[email protected]/umd/recoil.min.js"></script><script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">

// import ReactDOM from 'react-dom';
// import type {ReactElement} from 'react';
// import {atomFamily, RecoilRoot, useRecoilState} from 'recoil';

// This Stack Overflow snippet demo uses UMD modules instead of the above import statments
const {atomFamily, RecoilRoot, useRecoilState} = Recoil;

const textInputState = atomFamily<string, [id: string, defaultValue?: string]>({
  key: 'textInput',
  default: ([, defaultValue]) => defaultValue ?? '',
});

type TextInputProps = {
  id: string;
  defaultValue?: string;
};

function TextInput ({defaultValue = '', id}: TextInputProps): ReactElement {
  const [value, setValue] = useRecoilState(textInputState([id, defaultValue]));

  return (
    <div>
      <input
        type="text"
        onChange={ev => setValue(ev.target.value)}
        placeholder={defaultValue}
        {...{value}}
      />
    </div>
  );
}

function App (): ReactElement {
  const fields = [
    { id: 'foo', type: 'text', value: 'bar' },
    { id: 'hello', type: 'text', value: 'world' },
  ];

  return (
    <RecoilRoot>
      <h1>Custom defaults using atomFamily</h1>
      {fields.map(({id, value: defaultValue}) => (
        <TextInput key={id} {...{defaultValue, id}} />
      ))}
    </RecoilRoot>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

</script>

  • Related