В приложениях с redux многие разработчики испытывают дискомфорт в DX при написании async логики. Например, у нас есть todos-app с бекендом для хранения списков задач пользователей. Для того, чтобы сделать возможным async взаимодействие в redux необходимо написать свою middleware, либо взять из готовых. Как пример, будем использовать redux-thunk. Ниже приведен код, в котором обрабатывается логика добавления todo.

import {
  ADD_TODO_SUCCESS,
  ADD_TODO_FAILURE,
  ADD_TODO_STARTED,
} from './types';

import axios from 'axios';

const url = `https://jsonplaceholder.typicode.com/todos`

export const addTodo = ({ title, userId }) => {
  return dispatch => {
    dispatch(addTodoStarted());

    axios
      .post(url, {title, userId, completed: false})
      .then(res => {
        dispatch(addTodoSuccess(res.data));
      })
      .catch(err => {
        dispatch(addTodoFailure(err.message));
      });
  };
};

const addTodoSuccess = todo => ({
  type: ADD_TODO_SUCCESS,
  payload: {
    ...todo
  }
});

const addTodoStarted = () => ({
  type: ADD_TODO_STARTED
});

const addTodoFailure = error => ({
  type: ADD_TODO_FAILURE,
  payload: {
    error
  }
});

addTodo громоздкая thunk-function вызывающая actionCreators в зависимости от состояния Promise.

Давайте перепишем этот код с использованием Effector.js и Effects

<aside> 💡 Effect

</aside>

// добавить текст описывающий листинг

import {createEffect} from 'effector'

const url = `https://jsonplaceholder.typicode.com/todos`
// импортируем или создаем асинхронную функцию, которая взаимодействует с api
async function requestAddToDo({ title, userId }) {
	try {
		return await axios.post(url, { title, userId, completed: false })
	}
	catch (error) {
		throw error
	}
}

// создаем Effect
const fxAddTodo = createEffect(requestAddToDo)
// создаем Event, который вызовет Effect с переданным объектом
export const addTodo = fxAddTodo.prepend(payload => payload)

// вызываем Event
addTodo({title: 'Done Effector.js docs', userId: 1337 })

Обработка ответа запроса через Effect

Созданный Effect (addTodo) имеет свойства .done .fail .pending .finally. С помощью данных свойств нам не нужно создавать функции **addTodoSuccess, addTodoFailure, addTodoStarted. В конечном итоге отслеживание async действий сводиться к такой записи

// Example 1
const isLoading = createStore(false)
  .on(addTodo, () => true)
  .on([fxAddTodo.done, fxAddTodo.fail], () => false)
// или .reset(addTodo.done, addTodo.fail)

const error = createStore(null)
  .on(fxAddTodo.fail, (_, {error}) => error)
  .reset(addTodo.done)

const todosStatus = combine({
  loading: isLoading,
  error,
})
// Example 2
const error = createStore(null)
  .on(fxAddTodo.fail, (_, {error}) => error)
  .reset(fxAddTodo.done)

const todosStatus = combine({
  loading: fxAddTodo.pending,
  error,
})

В Example 1 создается Store isLoading. С помощью него можно отслеживать fetch данных. В последних версиях Effector.js было добавлено свойство pending. Пример его использования Example 2. addTodo.pending заменяет собой создание Store isLoading.

// Добавляем данные в стор и считываем все из него в View

const $todosList = createStore([]).on(fxAddTodo.doneData, (_, todos) => todos);

const $error = createStore(null)
  .on(fxAddTodo.fail, (_, {error}) => error)
  .reset(fxAddTodo.done)

const $todos = combine({
  loading: fxAddTodo.pending,
  error: $error
  data: $todosList
})

//View.jsx 

function View() {
  const {data: todos, loading, error} = useStore($todos)

	return <ul>{todos.map(todo => <li>{todo.title}</li>)}</ul>
}

Итого: