作者开发了一个名为“reactive-react-redux”的库,尽管它基于 Redux,但和传统方法又有一些区别。作者基于这个库给出了 Redux 中 Todo List 的示例代码。
如果你已经在用 React Redux 并爱上它,可能会不理解为什么人们尝试使用 React 中的 context 和 hook 来替换 Redux,即所谓“去 Redux 化”。
有些人认为 Redux DevTools 的扩展工具和中间件蛮不错的,对于他们来说,Redux 和 context + hook 实际上是两种选项。Context + hook 可以在组件之间实现状态共享,但是随着 APP 变得越来越大,有可能还是需要引入 Redux 或其他类似的解决方案,否则,最终运行中会出现太多上下文而无法进行顺畅处理。但是,我得承认这只是假设,将来或许能够找到更好的解决方案。
我最近一直在开发一个名为“reactive-react-redux”的库,尽管它基于 Redux,但和传统方法又有一些区别。Github 地址:https://github.com/dai-shi/reactive-react-redux
它的 API 非常简单直观,而且 Proxy 让它的性能得到了优化。我希望这个库能挽回一些用 context + hook 去替代 Redux 的开发人员,为此我还基于这个库写了代码示例。下面这个示例实现的是 Redux 中著名的 Todo List。
这个示例是用 TypeScript 语言写的。如果你不熟悉 TypeScript,请尝试忽略 State、Action 和*Type 这些关键字。
类型定义和状态还原器(reducer)
State 和 Action 的类型定义定义如下:
./src/types/index.ts
export type VisibilityFilterType =
| 'SHOW_ALL'
| 'SHOW_COMPLETED'
| 'SHOW_ACTIVE';
export type TodoType = {
id: number;
text: string;
completed: boolean;
};
export type State = {
todos: TodoType[];
visibilityFilter: VisibilityFilterType;
};
export type Action =
| { type: 'ADD_TODO'; id: number; text: string }
| { type: 'SET_VISIBILITY_FILTER'; filter: VisibilityFilterType }
| { type: 'TOGGLE_TODO'; id: number };
复制代码
状态还原器(reducer)的代码几乎与原始示例一样,如下所示。
./src/reducers/index.ts
import { combineReducers } from 'redux';
import todos from './todos';
import visibilityFilter from './visibilityFilter';
export default combineReducers({
todos,
visibilityFilter,
});
复制代码
./src/reducers/todos.ts
import { TodoType, Action } from '../types';
const todos = (state: TodoType[] = [], action: Action): TodoType[] => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false,
},
];
case 'TOGGLE_TODO':
return state.map((todo: TodoType) => (
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
));
default:
return state;
}
};
export default todos;
复制代码
./src/reducers/visibilityFilter.ts
import { Action, VisibilityFilterType } from '../types';
const visibilityFilter = (
state: VisibilityFilterType = 'SHOW_ALL',
action: Action,
): VisibilityFilterType => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};
export default visibilityFilter;
复制代码
动作生成器(Action creators)
有几种方法都可以用来实现动作分派。而我的选择是为每个动作创建 hook。注意,这方面我们仍在探索更好的实现方式。
./src/actions/index.ts
import { useCallback } from 'react';
import { useReduxDispatch } from 'reactive-react-redux';
import { Action, VisibilityFilterType } from '../types';
let nextTodoId = 0;
export const useAddTodo = () => {
const dispatch = useReduxDispatch<Action>();
return useCallback((text: string) => {
dispatch({
type: 'ADD_TODO',
id: nextTodoId++,
text,
});
}, [dispatch]);
};
export const useSetVisibilityFilter = () => {
const dispatch = useReduxDispatch<Action>();
return useCallback((filter: VisibilityFilterType) => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter,
});
}, [dispatch]);
};
export const useToggleTodo = () => {
const dispatch = useReduxDispatch<Action>();
return useCallback((id: number) => {
dispatch({
type: 'TOGGLE_TODO',
id,
});
}, [dispatch]);
};
复制代码
以上实现其实并非真正意义上的动作生成器,而是返回动作分派器的 hook。
组件
我们并不在这里区分演示组件(presentational components)和容器组件(container components)。当然如何构造组件仍然是个值得探讨的话题,但是在本例中,组件都被视为扁平的。
./src/components/App.tsx:App 也和原始示例保持一致。
import * as React from 'react';
import Footer from './Footer';
import AddTodo from './AddTodo';
import VisibleTodoList from './VisibleTodoList';
const App: React.FC = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
);
export default App;
复制代码
./src/components/Todo.tsx:这里做了一些小的修改,但没有特别大的改动。
import * as React from 'react';
type Props = {
onClick: (e: React.MouseEvent) => void;
completed: boolean;
text: string;
};
const Todo: React.FC<Props> = ({ onClick, completed, text }) => (
<li
onClick={onClick}
role="presentation"
style={{
textDecoration: completed ? 'line-through' : 'none',
cursor: 'pointer',
}}
>
{text}
</li>
);
export default Todo;
复制代码
./src/components/VisibleTodoList.tsx:这里并未出现 mapStateToProps 或 selector 函数,只是在 render 中调用 getVisibleTodos。
import * as React from 'react';
import { useReduxState } from 'reactive-react-redux';
import { TodoType, State, VisibilityFilterType } from '../types';
import { useToggleTodo } from '../actions';
import Todo from './Todo';
const getVisibleTodos = (todos: TodoType[], filter: VisibilityFilterType) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
default:
throw new Error(`Unknown filter: ${filter}`);
}
};
const VisibleTodoList: React.FC = () => {
const state = useReduxState<State>();
const visibleTodos = getVisibleTodos(state.todos, state.visibilityFilter);
const toggleTodo = useToggleTodo();
return (
<ul>
{visibleTodos.map(todo => (
<Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
))}
</ul>
);
};
export default VisibleTodoList;
复制代码
./src/components/FilterLink.tsx:同样,当 useReduxState 函数返回整个 Redux 状态对象时,程序只是使用其属性对 active 进行评估。
import * as React from 'react';
import { useReduxState } from 'reactive-react-redux';
import { useSetVisibilityFilter } from '../actions';
import { State, VisibilityFilterType } from '../types';
type Props = {
filter: VisibilityFilterType;
};
const FilterLink: React.FC<Props> = ({ filter, children }) => {
const state = useReduxState<State>();
const active = filter === state.visibilityFilter;
const setVisibilityFilter = useSetVisibilityFilter();
return (
<button
type="button"
onClick={() => setVisibilityFilter(filter)}
disabled={active}
style={{
marginLeft: '4px',
}}
>
{children}
</button>
);
};
export default FilterLink;
复制代码
./src/components/Footer.tsx:由于有类型检查的保证,可以将字符串传递给 FilterLink 组件的 filter 属性。
import * as React from 'react';
import FilterLink from './FilterLink';
const Footer: React.FC = () => (
<div>
<span>Show: </span>
<FilterLink filter="SHOW_ALL">All</FilterLink>
<FilterLink filter="SHOW_ACTIVE">Active</FilterLink>
<FilterLink filter="SHOW_COMPLETED">Completed</FilterLink>
</div>
);
export default Footer;
复制代码
./src/components/AddTodo.tsx:这里对原始示例进行了一些修改,以便使用带有 useState 的受控表单。
import * as React from 'react';
import { useState } from 'react';
import { useAddTodo } from '../actions';
const AddTodo = () => {
const [text, setText] = useState('');
const addTodo = useAddTodo();
return (
<div>
<form
onSubmit={(e) => {
e.preventDefault();
if (!text.trim()) {
return;
}
addTodo(text);
setText('');
}}
>
<input value={text} onChange={e => setText(e.target.value)} />
<button type="submit">Add Todo</button>
</form>
</div>
);
};
export default AddTodo;
复制代码
在线演示
请打开你的浏览器访问codesandbox,运行该示例。你也可以在GitHub上找到所有源代码。
其他信息
这篇文章中,并没有解释关于 reactive- response -redux 的内部细节。请访问GitHub查看更多信息。
英文原文:https://blog.axlight.com/posts/redux-meets-hooks-for-non-redux-users-a-small-concrete-example-with-reactive-react-redux/
评论