Динамические атомные клавиши в Recoil

Я пытаюсь создать динамическую форму, в которой поля ввода формы отображаются на основе данных, возвращаемых API.

Поскольку у атома должен быть уникальный ключ, я попытался обернуть его внутри функции, но каждый раз, когда я обновляю значение поля или компонент перемонтируется (попробуйте изменить вкладки), я получаю предупреждение:

Динамические атомные клавиши в Recoil

Я сделал небольшой рабочий пример здесь 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.info("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>
  );
}

Возможно ли это даже с отдачей. Я имею в виду, что это работает, но я не могу игнорировать предупреждения.

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
0
0
44
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Думаю проблема в textState(id, defaultValue). Каждый раз, когда вы запускаете повторный рендеринг для TextInput, эта функция будет вызываться снова, чтобы создать новый атом с тем же ключом.

Чтобы избежать такой ситуации, вы можете создать глобальную переменную для отслеживания добавленных atom. Например

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];
}

Спасибо и за ваш ответ. Я бы очень не решился полагаться на глобальные переменные в реагирующих приложениях, но, по крайней мере, это выполняет свою работу. Подход atomFamily кажется лучшим решением.

Dac0d3r 18.03.2022 21:41
Ответ принят как подходящий

Этот ответ показывает, как вы можете вручную управлять несколькими экземплярами атомов с помощью мемоизации.

Однако, если ваш defaultValue для каждого экземпляра использования не изменится, то Recoil уже предоставляет утилиту, которая может позаботиться об этом создании и запоминании для вас: atomFamily. Я процитирую некоторую соответствующую информацию из предыдущей ссылки (но прочитайте все, чтобы полностью понять):

... 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.

Вот рабочий пример, показывающий, как вы можете использовать свои id и defaultValue (уникальная комбинация значений в виде кортежа) в качестве параметра при использовании экземпляра состояния atomFamily для каждого ввода:

ТС игровая площадка

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>

Другие вопросы по теме