У вас есть компоненты React. Вы выяснили, как использовать Redux для передачи им данных. Вы создаете компонент, который создает / обновляет / удаляет элемент в вашем API, и помещаете его на страницу, отображающую элементы из вашего API. Как сделать так, чтобы вся страница перезагружалась, чтобы отображались обновленные данные каждый раз, когда пользователь вносит изменения? Состояние Redux и массив зависимостей useEffect().

Можно использовать только React для передачи данных из нашего счетчика в родительский элемент, чтобы получить почти такой же эффект счетчика в моем примере, но использование состояния Redux и массива зависимостей React useEffect() в некоторых отношениях проще и применимо ко многим другим ситуации.

Эта статья предполагает базовое знакомство с API, React, Redux и Node. Я запустил это приложение с create-react-app и использую функциональные компоненты и хуки. Вы можете найти весь код в этом репозитории. Я работаю с React (v17.0.1), Redux (v4.0.5), react-redux (v7.2.2) и redux-thunk (v2.3.0). В этом примере я использовал Dog API, чтобы отображать случайные изображения шиба-ину.

После тестирования конечных точек API (в данном случае в браузере с JSONView) я начал с настройки типов действий и создателей действий для запроса GET Dog API. Я могу создавать асинхронные действия благодаря redux-thunk. В этом небольшом примере я оставил свои запросы на выборку внутри создателей действий.

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

> shibaActions.js
function shibesRequested() { return { type: shibaConstants.SHIBES_REQUESTED } }
function success(result) { return { type: shibaConstants.SHIBES_FETCHED, payload: result } }
function failure(error) { return { type: shibaConstants.SHIBE_FETCH_FAIL, payload: error } }

Далее редуктор:

> shibaReducer.js
const reducer = (state = initialState, action) => {
    switch(action.type) {
        case shibaConstants.SHIBES_REQUESTED:
            return {
                ...state,
                shibasLoading: true,
                shibasFetched: false
            }
        case shibaConstants.SHIBES_FETCHED:
            return {
                ...state,
                shibasLoading: false,
                shibasFetched: true,
                shibas: action.payload
            }
        case shibaConstants.SHIBE_FETCH_FAIL:
            return {
                ...state,
                shibasLoading: false,
                shibasFetched: false
            }
        default:
            return state
    }
}

Наконец, я инициализирую магазин. Первый элемент в composeEnhancers включает Redux Dev Tools, и эта настройка позволяет использовать как инструменты разработчика, так и промежуточное ПО (в данном случае redux-thunk).

> index.js
import { compose, createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import reducer from './store/shibaReducer';
const rootReducer = reducer;
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

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

> App.js
import { connect } from 'react-redux';
import { shibaActions } from './store/shibaActions';
import Counter from './components/Counter';
function App(props) {
  const [number, setNumber] = useState(1);
useEffect(() => {
    props.fetchShibes(number)
  }, []);
if (!props.shibes || !props.shibes[0]) {
    return (<h1 className="heading">Loading!</h1>)
  } else {
    
    let shibaImages = props.shibes.map(shiba => {
      return (
        <img className="image" src={shiba} alt="shiba" key={shiba}>
        </img>
      );
    })
  
    return (
      <div className="App">
        {shibaImages}
        <Counter />
      </div>
    );
  }
}
const mapStateToProps = (state) => {
  return {
    shibes: state.shibas
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    fetchShibes: (num) => dispatch(shibaActions.fetchShibes(num))
  }
}
export default connect(mapStateToProps, mapDispatchToProps)(App);

Пустой useEffect() массив зависимостей (скобки после функции) означает, что страница не будет повторно визуализироваться бесконечно, и повторная визуализация не будет инициирована обновлениями зависимостей компонента. Примечание. Денни Скотт и команда React не рекомендуют пустые массивы зависимостей, поскольку они скрывают ошибки.

Теперь, когда мои шибки отображаются, я запускаю компонент счетчика. Поскольку этот API не имеет конечных точек POST, UPDATE или DELETE, счетчик изменит значение num, переданное на URL-адрес запроса GET в создателе действия.

let url = `https://dog.ceo/api/breed/shiba/images/random/${num}`;

Сначала я обновляю свои типы действий, создателей действий и редуктор. В этом небольшом примере я использую один файл для каждого из них, но обычно я использую несколько редукторов и использую combineReducers()hook.

> shibaActions.js
function addOne(num) {
      return dispatch => {
        let number = num + 1;
        dispatch(add(number))
      }
  function add(number)  { return { type: shibaConstants.ADD_ONE,  
  payload: number } }
}
function subOne(num) {
      return dispatch => {
          let number = num - 1;
          dispatch(sub(number))
      }
  function sub(number) { return { type: shibaConstants.SUB_ONE, 
  payload: number } }     
}
> shibaReducer.js
case shibaConstants.ADD_ONE:
   return {
       ...state,
       counter: action.payload
   }
case shibaConstants.SUB_ONE:
   return {
       ...state,
       counter: action.payload
   }

Еще добавляю счетчик в начальное состояние, чтобы всегда было 1 шибе.

> shibaReducer.js
const initialState = {shibasLoading: false, shibasFetched: false, counter: 1};

Теперь о самом счетчике - довольно просто.

> Counter.js
import React from 'react';
import { connect } from 'react-redux';
import { shibaActions } from '../store/shibaActions';
function Counter(props) {
    const handleOnAdd = () => {
        props.addOne(props.counter)
    }
const handleOnSub = () => {
        props.subOne(props.counter)
    }
return (
        <div className="container-counter">
            <h1 className="heading">Counter!</h1>
            <button onClick={handleOnAdd}>+</button>
            <p>{props.counter}</p>
            <button onClick={handleOnSub}>-</button>
        </div>
    )
}
const mapStateToProps = (state) => {
    return {
        counter: state.counter
    }
}
const mapDispatchToProps = (dispatch) => {
    return {
        addOne: (num) => dispatch(shibaActions.addOne(num)),
        subOne: (num) => dispatch(shibaActions.subOne(num))
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter);

Затем, чтобы наша страница перезагружалась каждый раз при нажатии кнопки изменения счетчика, мы должны вернуться к App.js. Мы будем использовать mapStateToProps() для доступа к счетчику в компоненте страницы и передавать его вместо локального состояния number при отправке fetchShibes().

> App.js
// const [number, setNumber] = useState(1);
useEffect(() => {
    props.fetchShibes(props.counter)
  }, [props.counter]);

Как вы можете видеть выше, единственное, что нам нужно сделать, чтобы перезагрузить страницу с изменениями кнопок, - это поместить состояние счетчика в useEffect() скобки зависимостей (команда React рекомендует назначать состояние переменным вместо использования props.state внутри массив зависимостей). Добавьте немного стиля, и у нас есть базовый маленький счетчик Шиба.

Перезагрузить страницу с помощью асинхронного действия лишь немного сложнее - вы должны использовать состояние загрузки, подобное shibasLoading выше. Если вы используете только состояние конечного результата, например shibasFetched, то страница будет перезагружена только при первом успешном запросе. Поэтому, если у вас есть пользователь, добавляющий несколько вещей в ваш API, он покажет только первое.

Эта концепция также позволяет создавать сообщения об ошибках в вашем приложении. После добавления объектов состояния shibasFetched и shibasLoading в mapStateToProps() я обновил свой файл App.js.

> App.js
if (!props.shibes || !props.shibes[0]) {
    return (
      <div>
        {(props.shibasLoading || !props.shibasFetched) && <h1  
        className="heading">Loading!</h1>}
        {!props.shibasLoading && !props.shibasFetched && 
        <h1>Something went wrong - shibas not loaded.</h1>}
      </div>
    )
  } else {
    
    let shibaImages = props.shibes.map(shiba => {
      return (
        <img className="image" src={shiba} alt="shiba" key={shiba}>
        </img>
      );
    })
  
    return (
      <div className="App">
        {!props.shibasLoading && !props.shibasFetched && 
        <h2>Something went wrong - shibas not loaded.</h2>}
        <Counter />
        {shibaImages}
      </div>
    );
  }

Используя условную логику JSX и наше состояние Redux, мы можем отображать различные сообщения об ошибках в зависимости от состояния наших асинхронных запросов.

Заключение

Пытаясь понять это сам, я попытался создать простой пример того, как использовать ловушку useEffect() для перезагрузки компонентов на основе изменений, вносимых другими компонентами.

Есть много способов использовать это в приложении, и сама базовая концепция позволяет делать забавные вещи, такие как настраиваемые сообщения об ошибках, с использованием условной логики JSX.

Если это помогло вам, когда вы застряли или оставили вопрос, оставьте мне комментарий!