Инициализация пользовательского модуля с асинхронными данными в перехватчике

#reactjs

#reactjs

Вопрос:

Во-первых, некоторый контекст.

У меня есть функциональный компонент, который загружает данные асинхронно.

 const { error, loading, data, getAllData } = useData(); // useData is a custom hook abstracting Context API

if (error) return <Error error={error} />;
if (!data || loading) return <LinearProgress />;

{/* etc .. */}
  

so data загружается асинхронно, что означает, что изначально он не определен, но когда асинхронная загрузка завершается, он получает некоторое реальное значение.

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

Итак, я в конечном итоге получаю

 const { error, loading, data, getAllData } = useData();
const tableCtrl = useTable(data);

if (error) return <Error error={error} />;
if (!data || loading) return <LinearProgress />;
  

В useTable.ts у меня есть

 export function useTable(initialItems: TableItem[]): TableCtrl {
    const [state, dispatch] = React.useReducer(reducer, {
        items: initialItems,
        selectedItems: [],
        allSelected: false
    });

    return {
        ...state,
        deleteItem: (itemToDelete: TableItem) =>
            dispatch({ type: 'delete_item', item: itemToDelete }),
        selectAll: () => dispatch({ type: 'select_all' }),
        selectNone: () => dispatch({ type: 'select_none' }),
        select: (item: TableItem) => dispatch({ type: 'select', item }),
        deselect: (item: TableItem) => dispatch({ type: 'deselect', item })
    };
}
  

Теперь я почти уверен, что вы можете определить проблему, но initialData который использует перехват для создания useReducer есть undefined , и он никогда не обновляет его. Поэтому, когда реальные данные «поступают», они не обновляют состояние перехвата.

Как я мог это исправить?

Ответ №1:

Это интересный вопрос. Поскольку перехват React.useReducer принимает начальное значение только один раз, он будет поддерживать само состояние без повторной установки значения.

Итак, идея довольно проста. Вы просто снова устанавливаете свое состояние с помощью dispatch функции, которую вы экспортировали каждый раз, когда ваше data было изменено. Вот фрагмент идеи:

Давайте создадим экспортируемую функцию для обновления вашего состояния в useTable.ts :

 
return {
 setData: data => dispatch(/* This is where you set your state again */)
}

  

Теперь setData исправьте свои data изменения в вашем основном файле:

 const { setData, ...others } = useTable(data);

React.useEffect(() => {
  if (data) {
   // This will sync your state with the latest data
   setData(data);
  }
}, [data])
  

Ответ №2:

В итоге я сделал то же самое, что и @tmhao2005. Мое окончательное решение выглядит следующим образом. Я хотел сделать это, чтобы иметь возможность отделять логику таблицы (выбор, удаление и т.д.) От логики представления. Чтобы я мог создать компонент generic-table’ish (не показан в примере ниже)

 const rootElement = document.getElementById("root");
const {
  Table,
  TableBody,
  TableContainer,
  TableHead,
  TableCell,
  TableRow,
  Link
} = MaterialUI;

// to mimic async loading of data
function useData() {
  const [data, setData] = React.useState([]);

  React.useEffect(() => {
    setTimeout(() => setData([1, 2, 3]), 0);
  }, []);

  return {
    data,
    deleteByIndex: (idx) => {
      // some random function to mutate the state
      setData(data.filter((_, index) => idx !== index));
    }
  };
}

function reducer(state, action) {
  switch (action.type) {
    case "init":
      return { ...state, items: action.items };
    default:
      throw new Error("unknown action");
  }
}

function useTable(initialItems) {
  const isFunctionCreator = React.useMemo(
    () => typeof initialItems === "function",
    [initialItems]
  );

  const [state, dispatch] = React.useReducer(reducer, {
    items: isFunctionCreator ? null : initialItems,
    selectedItems: [],
    allSelected: false
  });

  const data = isFunctionCreator ? initialItems() : null;

  React.useEffect(() => {
    if (isFunctionCreator amp;amp; data) {
      dispatch({ type: "init", items: data });
    }
  }, [isFunctionCreator, data]);

  return {
    ...state
  };
}


const App = () => {
  const { data, deleteByIndex } = useData();
  const ctrl = useTable(() => data);

  if (!data) return <div>Loading</div>;

  return (
    <TableContainer>
      <Table>
        <TableHead>
          <TableRow>
            <TableCell>Item</TableCell>
            <TableCell align="center" size="small"></TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {ctrl.items amp;amp;
            ctrl.items.map((item, idx) => (
              <TableRow key={item}>
                <TableCell>{item}</TableCell>
                <TableCell align="center" size="small">
                  <Link onClick={() => deleteByIndex(idx)}>Delete</Link>
                </TableCell>
              </TableRow>
            ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

// React bootstrap
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  rootElement
);  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.production.min.js"></script>

<div id="root"></div>  

Комментарии:

1. Способ просмотра начального значения на вашем перехвате, я думаю, тоже неплохая идея 🙂