План
- создать проект с помощью приложения create-реагировать и установить зависимости
- удалить ненужные файлы и код
- создать магазин с редуксом и сделать его доступным в нашем приложении
- создавать типы
- добавить некоторые действия в наш магазин и управлять состоянием
- создавать компоненты и отправлять действия
Создайте проект реакции и установите зависимости
Перейдите в папку вашего проекта, откройте терминал и запустите
npx create-react-app . --template typescript
Теперь установите зависимости
npm i redux react-redux @types/react-redux redux-devtools-extension bulma @fortawesome/fontawesome-free
Поскольку мы будем использовать typescript, нам нужно установить типы для пакета react-redux. Нам также нужно установить bulma, потому что мы будем использовать bulma css framework для пользовательского интерфейса и fontawesome для использования иконок.
Удалить ненужные файлы и код
Удалите файлы App.test.tsx, logo.svg, index.css, serviceWorker.ts и setupTests.ts.
Удалите все из файла App.css. Удалите импорт логотипа из файла App.tsx и всю разметку внутри div с классом App, пока просто оставьте этот div.
И из файла index.tsx удалите импорт serviceWorker и код serviceWorker и импорт index.css.
Теперь App.tsx должен выглядеть так:
import React from 'react'; import './App.css';
function App() { return ( <div className="App"> </div> ); }
export default App;
index.tsx:
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App';
ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') );
Создайте магазин с редуксом и сделайте его доступным в нашем приложении.
Внутри папки src создайте папку хранилища, а внутри папки хранилища — папки действий и редукторов, а также файлы store.ts и types.ts.
Внутри папки редукторов создайте 2 файла, listReducer.ts иnotificationReducer.ts.
А пока добавьте этот код в оба этих файла:
export default (state, action) => {
switch(action.type) {
default:
return state;
}
}
Typescript будет жаловаться на типы состояний и действий, но не волнуйтесь, мы добавим это через минуту.
Теперь давайте перейдем к файлу store.ts и добавим внутрь этот код:
import { createStore, combineReducers } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension';
import listReducer from './reducers/listReducer'; import notificationReducer from './reducers/notificationReducer';
const rootReducer = combineReducers({ list: listReducer, notification: notificationReducer });
const store = createStore(rootReducer, composeWithDevTools());
export type RootState = ReturnType<typeof rootReducer>;
export default store;
Здесь нам нужно объединить два наших редуктора и создать хранилище с помощью функции createStore. Я также использую расширение redux devtools, чтобы проверить в своем браузере, какие типы были отправлены.
Нам также нужно экспортировать тип RootState, который является типом rootReducer, чтобы использовать его в наших компонентах всякий раз, когда мы будем использовать хук useSelector.
И, наконец, мы можем экспортировать наш магазин по умолчанию.
Теперь магазин создан, и теперь нам нужно сделать его доступным в нашем приложении.
Теперь давайте вернемся к файлу index.tsx и импортируем Provider из react-redux, обернем им наше приложение и добавим наш магазин в качестве реквизита. Нам также нужно импортировать наш магазин, и поскольку мы обновляем этот файл, мы также можем импортировать bulma и fontawesome.
index.tsx должен выглядеть так:
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux';
import '../node_modules/bulma/css/bulma.min.css'; import '../node_modules/@fortawesome/fontawesome-free/css/all.min.css';
import store from './store/store'; import App from './App';
ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') );
Создание типов
Перейдите к файлу types.ts в папке магазина и добавьте следующие типы:
export const GET_LISTS = 'GET_LISTS'; export const GET_LIST_BY_ID = 'GET_LIST_BY_ID'; export const ADD_LIST = 'ADD_LIST'; export const DELETE_LIST = 'DELETE_LIST'; export const UPDATE_LIST = 'UPDATE_LIST'; export const SET_LISTID_TO_DELETE = 'SET_LISTID_TO_DELETE'; export const SET_LIST_TO_EDIT = 'SET_LISTID_TO_EDIT'; export const SET_SELECTED_LIST = 'SET_SELECTED_LIST';
export const ADD_TASK = 'ADD_TASK'; export const DELETE_TASK = 'DELETE_TASK'; export const SET_TASK_TO_DELETE = 'SET_TASK_TO_DELETE'; export const UNSET_TASK_TO_DELETE = 'UNSET_TASK_TO_DELETE'; export const UPDATE_TASK = 'UPDATE_TASK'; export const SET_TASK_TO_EDIT = 'SET_TASK_TO_EDIT'; export const UNSET_TASK_TO_EDIT = 'UNSET_TASK_TO_EDIT';
export const SET_NOTIFICATION = 'SET_NOTIFICATION';
Это все типы, которые мы собираемся использовать в нашем приложении, в действиях и редюсерах.
Давайте создадим несколько интерфейсов для наших задач и списков. Добавьте их ниже типов магазинов.
export interface Task { name: string; id: string; completed: boolean; }
export interface List { name: string; id: string; tasks: Task[]; }
export interface Lists { [id: string]: List }
Каждая задача будет иметь имя, которое является строкой, идентификатор, который является строкой, и завершение, которое является логическим значением.
Список также будет иметь имя и идентификатор, оба из которых являются строками, и задачи, которые представляют собой массив задач.
Мы будем хранить наши списки в локальном хранилище как объект, который будет иметь ключи (идентификатор списка) и значения (фактический список). Поэтому для этого нам нужен интерфейс Lists. id будет динамическим, поэтому нам нужно использовать [].
Для каждого действия нам также нужно создать интерфейс, каждое действие будет возвращать тип и полезную нагрузку.
// Actions interface AddListAction { type: typeof ADD_LIST; payload: List; }
interface GetListsAction { type: typeof GET_LISTS; }
interface GetListByIdAction { type: typeof GET_LIST_BY_ID; payload: string; }
interface SetListIdToDeleteAction { type: typeof SET_LISTID_TO_DELETE; payload: string; }
interface SetListToEditAction { type: typeof SET_LIST_TO_EDIT; payload: string; }
interface DeleteListAction { type: typeof DELETE_LIST; payload: string; }
interface UpdateListAction { type: typeof UPDATE_LIST; payload: { id: string; name: string; } }
interface SetSelectedListAction { type: typeof SET_SELECTED_LIST; payload: string; }
interface SetNotificationAction { type: typeof SET_NOTIFICATION; payload: { msg: string; type: string; }; }
interface AddTaskAction { type: typeof ADD_TASK; payload: { task: Task; list: List; } }
interface DeleteTaskAction { type: typeof DELETE_TASK; payload: { task: Task; list: List; }; }
interface SetTaskToDeleteAction { type: typeof SET_TASK_TO_DELETE; payload: { task: Task; list: List; }; }
interface UnsetTaskToDeleteAction { type: typeof UNSET_TASK_TO_DELETE; }
interface EditTaskAction { type: typeof UPDATE_TASK; payload: { taskId: string; taskName: string; taskState: boolean; list: List; } }
interface SetTaskToEditAction { type: typeof SET_TASK_TO_EDIT; payload: { task: Task; list: List; }; }
interface UnsetTaskToEditAction { type: typeof UNSET_TASK_TO_EDIT; }
export type ListsAction = AddListAction | GetListsAction | GetListByIdAction | SetListIdToDeleteAction | SetListToEditAction | DeleteListAction | UpdateListAction | SetSelectedListAction | AddTaskAction | DeleteTaskAction | SetTaskToDeleteAction | UnsetTaskToDeleteAction | EditTaskAction | SetTaskToEditAction | UnsetTaskToEditAction; export type NotificationAction = SetNotificationAction;
Как видите, действий у нас будет много. Каждый тип действия является строкой, но нам нужно использовать typeof, а затем тип, иначе typescript будет жаловаться и не будет знать, какой тип мы хотим использовать, потому что мы экспортируем все типы как тип объединения, и мы будем использовать только этот тип объединения в нашем действия.
И, наконец, давайте создадим интерфейсы для наших состояний. У нас будет 2 состояния, одно для списка и одно для уведомления.
export interface ListState { lists: Lists; listIdToDelete: string; listToEdit: List | null; listById: List | null; selectedList: List | null; taskToDelete: { task: Task; list: List; } | null; taskToEdit: { task: Task; list: List; } | null; }
export interface NotificationState { message: string; type: string; }
ListState будет иметь много свойств. У него будут списки, которые вернут все списки из локального хранилища, listIdToDelete, который будет установлен, когда мы нажмем кнопку удаления в нашем списке, и мы установим его до открытия модального окна, listToEdit, который будет установлен, когда мы нажмем имя списка, а затем мы можем использовать это в нашем модальном списке редактирования, selectedList, который изменится, когда мы изменим поле выбора, taskToDelete, который будет установлен, когда мы нажмем кнопку удаления задачи, и taskToEdit, который будет установлен, когда мы нажмем кнопку редактирования задачи.
NotificationState будет иметь только 2 свойства, сообщение и тип, и оба они являются строками.
Добавьте некоторые действия в наш магазин и управляйте состоянием
Внутри папки действий создайте файлы listActions.ts,notificationActions.ts и index.ts.
listActions.ts
import { List, ListsAction, Task, ADD_LIST, GET_LISTS, DELETE_LIST, SET_LISTID_TO_DELETE, GET_LIST_BY_ID, SET_LIST_TO_EDIT, UPDATE_LIST, SET_SELECTED_LIST, ADD_TASK, SET_TASK_TO_DELETE, DELETE_TASK, UNSET_TASK_TO_DELETE, SET_TASK_TO_EDIT, UNSET_TASK_TO_EDIT, UPDATE_TASK } from '../types';
export const addList = (list: List): ListsAction => { return { type: ADD_LIST, payload: list } }
export const getLists = (): ListsAction => { return { type: GET_LISTS } }
export const getListById = (id: string): ListsAction => { return { type: GET_LIST_BY_ID, payload: id } }
export const setListIdToDelete = (id: string): ListsAction => { return { type: SET_LISTID_TO_DELETE, payload: id } }
export const setListToEdit = (id: string): ListsAction => { return { type: SET_LIST_TO_EDIT, payload: id } }
export const setSelectedList = (id: string): ListsAction => { return { type: SET_SELECTED_LIST, payload: id } }
export const deleteList = (id: string): ListsAction => { return { type: DELETE_LIST, payload: id } }
export const updateList = (id: string, name: string): ListsAction => { return { type: UPDATE_LIST, payload: { id, name } } }
export const addTask = (task: Task, list: List): ListsAction => { return { type: ADD_TASK, payload: { task, list } } }
export const setTaskToDelete = (task: Task, list: List): ListsAction => { return { type: SET_TASK_TO_DELETE, payload: { task, list } } }
export const unsetTaskToDelete = (): ListsAction => { return { type: UNSET_TASK_TO_DELETE } }
export const deleteTask = (task: Task, list: List): ListsAction => { return { type: DELETE_TASK, payload: { task, list } } }
export const setTaskToEdit = (task: Task, list: List): ListsAction => { return { type: SET_TASK_TO_EDIT, payload: { task, list } } }
export const unsetTaskToEdit = (): ListsAction => { return { type: UNSET_TASK_TO_EDIT } }
export const updateTask = (taskId: string, taskName: string, taskState: boolean, list: List): ListsAction => { return { type: UPDATE_TASK, payload: { taskId, taskName, taskState, list } } }
Сначала нам нужно импортировать наши интерфейсы, тип объединения для действий списка и типы хранения. Затем мы можем создать действие, которое мы отправим в наши компоненты. Многие действия возвращают тип и полезную нагрузку, некоторые просто печатают. И все они вернут ListsAction, а поскольку мы использовали typeof, то машинописный текст будет знать, какой тип использовать для действия.
Например, в действии addList мы используем ListsAction в качестве возвращаемого типа, но поскольку мы используем ADD_LIST в качестве типа, машинописный текст будет знать, что мы хотим использовать AddListAction.
listReducer.ts
import { ListState, ListsAction, Lists, ADD_LIST, GET_LISTS, SET_LISTID_TO_DELETE, DELETE_LIST, GET_LIST_BY_ID, SET_LIST_TO_EDIT, UPDATE_LIST, SET_SELECTED_LIST, ADD_TASK, DELETE_TASK, SET_TASK_TO_DELETE, UNSET_TASK_TO_DELETE, SET_TASK_TO_EDIT, UNSET_TASK_TO_EDIT, UPDATE_TASK } from '../types';
const initialState: ListState = { lists: {}, listIdToDelete: '', listToEdit: null, listById: null, selectedList: null, taskToDelete: null, taskToEdit: null }
const getListsFromLS = (): Lists => { if(localStorage.getItem('task_list')) { return JSON.parse(localStorage.getItem('task_list') || '{}'); }
return {}; }
const saveListsToLS = (lists: Lists) => { localStorage.setItem('task_list', JSON.stringify(lists)); }
export default (state = initialState, action: ListsAction): ListState => { const listsFromLS = getListsFromLS(); switch(action.type) { case ADD_LIST: const copiedListsFromLS = {...listsFromLS}; copiedListsFromLS[action.payload.id] = action.payload; saveListsToLS(copiedListsFromLS); return { ...state, lists: copiedListsFromLS };
case GET_LISTS: return { ...state, lists: listsFromLS }
case GET_LIST_BY_ID: const list = listsFromLS[action.payload]; return { ...state, listById: list }
case SET_LISTID_TO_DELETE: return { ...state, listIdToDelete: action.payload }
case SET_LIST_TO_EDIT: const listToEdit = listsFromLS[action.payload] || null; return { ...state, listToEdit: listToEdit }
case DELETE_LIST: const copiedListsFromLS2 = {...listsFromLS}; const listId = copiedListsFromLS2[action.payload].id; delete copiedListsFromLS2[action.payload]; saveListsToLS(copiedListsFromLS2); return { ...state, lists: copiedListsFromLS2, listIdToDelete: '', listById: null, selectedList: state.selectedList && listId === state.selectedList.id ? null : state.selectedList }
case UPDATE_LIST: const copiedListsFromLS3 = {...listsFromLS}; copiedListsFromLS3[action.payload.id].name = action.payload.name; saveListsToLS(copiedListsFromLS3); return { ...state, lists: copiedListsFromLS3, listToEdit: null }
case SET_SELECTED_LIST: const selectedList = getListsFromLS()[action.payload]; return { ...state, selectedList: selectedList }
case ADD_TASK: const copiedListsFromLS4 = {...listsFromLS}; copiedListsFromLS4[action.payload.list.id].tasks.push(action.payload.task); saveListsToLS(copiedListsFromLS4); return { ...state, lists: copiedListsFromLS4, selectedList: copiedListsFromLS4[action.payload.list.id] }
case SET_TASK_TO_DELETE: return { ...state, taskToDelete: { task: action.payload.task, list: action.payload.list } }
case UNSET_TASK_TO_DELETE: return { ...state, taskToDelete: null }
case DELETE_TASK: const copiedListsFromLS5 = {...listsFromLS}; const copiedTasks = [...copiedListsFromLS5[state.taskToDelete!.list.id].tasks]; const task = copiedTasks.find(task => task.id === state.taskToDelete!.task.id); copiedTasks.splice(copiedTasks.indexOf(task!), 1); copiedListsFromLS5[state.taskToDelete!.list.id].tasks = copiedTasks; saveListsToLS(copiedListsFromLS5); return { ...state, lists: copiedListsFromLS5, selectedList: copiedListsFromLS5[state.taskToDelete!.list.id], taskToDelete: null }
case SET_TASK_TO_EDIT: return { ...state, taskToEdit: { task: action.payload.task, list: action.payload.list } }
case UNSET_TASK_TO_EDIT: return { ...state, taskToEdit: null }
case UPDATE_TASK: const copiedListsFromLS6 = {...listsFromLS}; const copiedList = {...copiedListsFromLS6[action.payload.list.id]}; const copiedTasks2 = [...copiedList.tasks]; const task2 = copiedTasks2.find((task) => task.id === action.payload.taskId); const copiedTask = {...task2!}; copiedTask.name = action.payload.taskName; copiedTask.completed = action.payload.taskState; const updatedTasks = copiedTasks2.map(task => task.id === copiedTask.id ? copiedTask : task); copiedList.tasks = updatedTasks; copiedListsFromLS6[copiedList.id] = copiedList; saveListsToLS(copiedListsFromLS6);
return { ...state, lists: copiedListsFromLS6, selectedList: copiedList, taskToEdit: null }
default: return state; } }
Сначала нам нужно импортировать некоторые интерфейсы и типы. Затем мы можем создать наше InitialState, которое будет иметь все свойства, которые мы определили в нашем интерфейсе ListState. И тогда наше состояние в редюсере может быть равно этому объекту initialState и действие может использовать тип ListsAction и редьюсер всегда будет возвращать ListState.
В этом файле я создал две вспомогательные функции: getListsFromLS, которая будет возвращать списки из локального хранилища или пустого объекта, и saveListsToLS, которая будет сохранять списки в локальное хранилище.
Теперь давайте добавим несколько случаев для каждого типа:
ADD_LIST
добавит список, который мы передали действию addList в качестве аргумента свойства lists, и сохранит его в локальном хранилище. Я всегда буду клонировать/копировать исходное значение, потому что я хочу изменить не исходное значение, а клонированное значение.
GET_LISTS
получит списки из локального хранилища и обновит свойство lists в состоянии списка
GET_LIST_BY_ID
получит список по id, который мы передали в нашем экшене в качестве параметра
SET_LISTID_TO_DELETE
установит listIdToDelete в идентификатор списка, который мы передали в качестве параметра
SET_LIST_TO_EDIT
получит список из локального хранилища по идентификатору, который мы передали в качестве параметра в нашем действии, и установит этот список в свойство listToEdit
DELETE_LIST
удалит список из локального хранилища, обновит свойство lists, установит listIdToDelete в пустую строку, установит для listById значение null и изменит selectedList на null, если список, который мы удалили, был выбран в поле выбора
ОБНОВЛЕНИЕ_СПИСКА
обновит список и сохранит обновленные списки в локальном хранилище, а также установит для listToEdit значение null
SET_SELECTED_LIST
обновит selectedList при изменении значения поля выбора
ADD_TASK
добавит новую задачу в выбранный список
SET_TASK_TO_DELETE
установит свойство задачи taskToDelete на задачу, которую мы хотим удалить, и свойство списка на выбранный список
UNSET_TASK_TO_DELETE
установит для taskToDelete значение null
DELETE_TASK
удалит выбранную задачу из выбранного списка и обновит списки в локальном хранилище
SET_TASK_TO_EDIT
установит свойство задачи taskToEdit для задачи, которую мы хотим отредактировать, и свойство list для выбранного списка
UNSET_TASK_TO_EDIT
установит для taskToEdit значение null
ОБНОВЛЕНИЕ_ТАСК
обновит выбранную задачу в выбранном списке и обновит списки в локальном хранилище
notificationActions.ts
import { SET_NOTIFICATION, NotificationAction } from '../types';
export const setNotification = (msg: string, type: string = 'success'): NotificationAction => { return { type: SET_NOTIFICATION, payload: { msg, type } } }
У нас есть только одно действие в этом файле, и это действие будет отправляться каждый раз, когда мы добавляем, удаляем или редактируем задачу или список. Он имеет 2 параметра: msg — это сообщение, которое мы хотим отобразить, и тип, который будет использоваться для отображения зеленого или красного цвета фона уведомления.
notificationReducer.ts
import { SET_NOTIFICATION, NotificationAction, NotificationState } from '../types';
const initialState: NotificationState = { message: '', type: 'success' }
export default (state = initialState, action: NotificationAction): NotificationState => { switch(action.type) { case SET_NOTIFICATION: return { ...state, message: action.payload.msg, type: action.payload.type } default: return state; } }
Это очень просто. У него есть один случай, SET_NOTIFICATION, и он будет устанавливать/обновлять сообщение и тип состояния уведомления.
index.ts в папке действий
export * from './listActions';
export * from './notificationActions';
В этом файле мы просто хотим экспортировать все из listActions и notificationActions, чтобы мы всегда могли импортировать действия из этого файла в наши компоненты.