Конфликты между useEffect в React

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

У меня есть это:

const PaginationComponent = ({minPrice, maxPrice}) => {

  const[page, setPage] = useState(null);
  const[items, setItems] = useState([]);

  const fetchMore = useCallback(() => {
    setPage(prevState => prevState + 1);
  }, []);

  useEffect(() => {
    if (page === null) {
      setPage(0);
      setItems([]);
    } else {
      get(page, minPrice, maxPrice)
        .then(response => setItems(response));
    }
  }, [page, minPrice, maxPrice]);

  useEffect(() => {
    setPage(null);
  },[minPrice, maxPrice]);
};

.. и есть проблема, потому что первый useEffect зависит от props, потому что я использую их для фильтрации данных, а во втором я хочу сбросить компонент. И в результате после изменения props запускаются оба useEffects.

У меня больше нет идей, как это сделать правильно.


person Bambelal    schedule 18.05.2020    source источник
comment
Я не уверен, нужен ли нам второй useEffect. При изменении реквизита дочерние элементы повторно визуализируются.   -  person Plum    schedule 18.05.2020
comment
Почему вы setPage(null), когда меняются minPrice и maxPrice, а затем выполняете запрос на выборку в другом useEffect хуке, когда те же самые свойства меняются?   -  person goto1    schedule 18.05.2020
comment
Мне нужно установить для страницы значение null, потому что что, если кто-то изменит фильтры на странице 0? :) 0 равно 0 :)   -  person Bambelal    schedule 18.05.2020


Ответы (2)


В общем, ключевым моментом здесь является перемещение состояния page до родительского компонента и изменение страницы на 0 всякий раз, когда вы меняете фильтры. Вы можете сделать это либо с useState, либо с useReducer.

Причина, по которой он работает с useState (т.е. есть только один повторный рендеринг), заключается в том, что React меняет состояние пакетов в обработчиках событий, если бы этого не было, вы все равно получили бы два вызова API. CodeSandbox

const PaginationComponent = ({ page, minPrice, maxPrice, setPage }) => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    get(page, minPrice, maxPrice).then(response => setItems(response));
  }, [page, minPrice, maxPrice]);

  return (
    <div>
      {items.map(item => (
        <div key={item.id}>
          {item.id}, {item.name}, ${item.price}
        </div>
      ))}
      <div>Page: {page}</div>
      <button onClick={() => setPage(v => v - 1)}>back</button>
      <button onClick={() => setPage(v => v + 1)}>next</button>
    </div>
  );
};

const App = () => {
  const [page, setPage] = useState(0);
  const [minPrice, setMinPrice] = useState(25);
  const [maxPrice, setMaxPrice] = useState(50);

  return (
    <div>
      <div>
        <label>Min price:</label>
        <input
          value={minPrice}
          onChange={event => {
            const { value } = event.target;
            setMinPrice(parseInt(value, 10));
            setPage(0);
          }}
        />
      </div>
      <div>
        <label>Max price:</label>
        <input
          value={maxPrice}
          onChange={event => {
            const { value } = event.target;
            setMaxPrice(parseInt(value, 10));
            setPage(0);
          }}
        />
      </div>

      <PaginationComponent minPrice={minPrice} maxPrice={maxPrice} page={page} setPage={setPage} />
    </div>
  );
};

export default App;


Другое решение - использовать useReducer, который более прозрачен, но также, как обычно с редукторами, немного тяжелее для шаблона. Этот пример ведет себя немного иначе, потому что есть кнопка «установить фильтры», которая изменяет состояние, которое передается компоненту разбиения на страницы, немного более «реальный» сценарий IMO. CodeSandbox

const PaginationComponent = ({ tableConfig, setPage }) => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    const { page, minPrice, maxPrice } = tableConfig;
    get(page, minPrice, maxPrice).then(response => setItems(response));
  }, [tableConfig]);

  return (
    <div>
      {items.map(item => (
        <div key={item.id}>
          {item.id}, {item.name}, ${item.price}
        </div>
      ))}
      <div>Page: {tableConfig.page}</div>
      <button onClick={() => setPage(v => v - 1)}>back</button>
      <button onClick={() => setPage(v => v + 1)}>next</button>
    </div>
  );
};

const tableStateReducer = (state, action) => {
  if (action.type === "setPage") {
    return { ...state, page: action.page };
  }
  if (action.type === "setFilters") {
    return { page: 0, minPrice: action.minPrice, maxPrice: action.maxPrice };
  }
  return state;
};

const App = () => {
  const [tableState, dispatch] = useReducer(tableStateReducer, {
    page: 0,
    minPrice: 25,
    maxPrice: 50
  });
  const [minPrice, setMinPrice] = useState(25);
  const [maxPrice, setMaxPrice] = useState(50);

  const setPage = useCallback(
    page => {
      if (typeof page === "function") {
        dispatch({ type: "setPage", page: page(tableState.page) });
      } else {
        dispatch({ type: "setPage", page });
      }
    },
    [tableState]
  );

  return (
    <div>
      <div>
        <label>Min price:</label>
        <input
          value={minPrice}
          onChange={event => {
            const { value } = event.target;
            setMinPrice(parseInt(value, 10));
          }}
        />
      </div>
      <div>
        <label>Max price:</label>
        <input
          value={maxPrice}
          onChange={event => {
            const { value } = event.target;
            setMaxPrice(parseInt(value, 10));
          }}
        />
      </div>
      <button
        onClick={() => {
          dispatch({ type: "setFilters", minPrice, maxPrice });
        }}
      >
        Set filters
      </button>

      <PaginationComponent tableConfig={tableState} setPage={setPage} />
    </div>
  );
};

export default App;
person paolostyle    schedule 18.05.2020

Вы можете использовать следующие

const fetchData = () => {
 get(page, minPrice, maxPrice)
    .then(response => setItems(response));
}
// Whenever page updated fetch new data
useEffect(() => {
 fetchData();
}, [page]);

// Whenever filter updated reseting page
useEffect(() => {
 const prevPage = page;
 setPage(0);
 if(prevPage === 0 ) {
   fetchData();
 }
},[minPrice, maxPrice]);
person Umair Farooq    schedule 18.05.2020
comment
Но первый эффект тоже зависит от minPrice и maxPrice. Так что он должен это слушать. - person Bambelal; 18.05.2020
comment
В соответствии с вашим вопросом, когда меняются фильтры, вам нужно только сбросить страницу, второй эффект меняет страницу. Затем будет выполнен первый эффект (поскольку страница изменилась), затем будет выполняться функция get для получения ответа. Значение minPrice и maxPrice по-прежнему будет передано в get функцию. - person Umair Farooq; 18.05.2020
comment
Хорошо, в этом случае предупреждение линтера не важно? Но как я уже писал выше - не могу установить Page на 0 .. - person Bambelal; 18.05.2020
comment
Он не может установить страницу для 0, потому что что, если я сейчас на странице 0 в моей разбивке на страницы? Нет никакой разницы, поэтому useEffect не запускается. - person Bambelal; 18.05.2020
comment
Я обновил сообщение, это может решить вашу проблему - person Umair Farooq; 18.05.2020