简体   繁体   English

Recoil 中的动态原子键

[英]Dynamic atom keys in Recoil

I'm trying to make a dynamic form where the form input fields is rendered from data returned by an API.我正在尝试制作一个动态表单,其中表单输入字段是根据 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:由于 atom 需要有一个唯一的键,我尝试将它包装在 function 中,但每次我更新字段值或组件重新安装(尝试更改选项卡)时,我都会收到一条警告:

“”

I made a small running example herehttps://codesandbox.io/s/zealous-night-e0h4jt?file=/src/App.tsx (same code as below):我在这里做了一个小的运行示例https://codesandbox.io/s/zealous-night-e0h4jt?file=/src/App.tsx (与下面相同的代码):

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.我的意思是它似乎有效,但我不能忽略警告。

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 .但是,如果您的每个使用实例的defaultValue都不会改变,那么 Recoil 已经提供了一个可以为您处理此创建和记忆的实用程序: 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.但是, Recoil通过atomFamily实用程序为您提供了这种模式。 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.当您调用atomFamily时,它将返回一个 function,它根据您传入的参数提供RecoilState原子。

The atomFamily essentially provides a map from the parameter to an atom. atomFamily本质上提供了一个 map 从参数到一个原子。 You only need to provide a single key for the atomFamily and it will generate a unique key for each underlying atom.您只需为atomFamily提供一个键,它将为每个底层原子生成一个唯一的键。 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.因此,对于atomFamily参数,使用值相等性而不是引用相等性。 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. atomFamily接受原始类型,或 arrays 或可以包含 arrays 的对象,对象或原始类型。

Here's a working example showing how you can use your id and defaultValue (a unique combination of values as a tuple) as a parameter when using an instance of atomFamily state for each input:这是一个工作示例,展示了在为每个输入使用atomFamily state 实例时如何使用iddefaultValue (值的唯一组合作为元组)作为参数:

TS Playground TS游乐场

 body { font-family: sans-serif; } input[type="text"] { font-size: 1rem; padding: 0.5rem; }
 <div id="root"></div><script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/recoil@0.6.1/umd/recoil.min.js"></script><script src="https://unpkg.com/@babel/standalone@7.17.7/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>

I think the problem is from textState(id, defaultValue) .我认为问题出在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.每次触发TextInput的重新渲染时,都会再次调用 function 以使用相同的键创建一个新原子。

To avoid that situation, you can create a global variable to track which atom added.为避免这种情况,您可以创建一个全局变量来跟踪添加了哪个atom 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];
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM