Redux+Hook 重写 Todo List,用代码实例挽回摒弃 Redux 的用户
作者开发了一个名为“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
复制代码
exporttypeVisibilityFilterType = |'SHOW_ALL' |'SHOW_COMPLETED' |'SHOW_ACTIVE'; exporttypeTodoType = { id:number; text:string; completed:boolean; }; exporttypeState = { todos: TodoType[]; visibilityFilter: VisibilityFilterType; }; exporttypeAction = | {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'; importtodosfrom'./todos'; importvisibilityFilterfrom'./visibilityFilter'; exportdefaultcombineReducers({ 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': returnstate.map((todo: TodoType) => ( todo.id === action.id ? { ...todo, completed: !todo.completed } : todo )); default: returnstate; } }; exportdefaulttodos;
./src/reducers/visibilityFilter.ts
复制代码
import{ Action, VisibilityFilterType }from'../types'; constvisibilityFilter = ( state: VisibilityFilterType ='SHOW_ALL', action: Action, ):VisibilityFilterType=>{ switch(action.type) { case'SET_VISIBILITY_FILTER': returnaction.filter; default: returnstate; } }; exportdefaultvisibilityFilter;
动作生成器(Action creators)
有几种方法都可以用来实现动作分派。而我的选择是为每个动作创建 hook。注意,这方面我们仍在探索更好的实现方式。
./src/actions/index.ts
复制代码
import{ useCallback }from'react'; import{ useReduxDispatch }from'reactive-react-redux'; import{ Action, VisibilityFilterType }from'../types'; letnextTodoId =0; exportconstuseAddTodo =()=>{ constdispatch = useReduxDispatch(); returnuseCallback((text:string) =>{ dispatch({ type:'ADD_TODO', id: nextTodoId++, text, }); }, [dispatch]); }; exportconstuseSetVisibilityFilter =()=>{ constdispatch = useReduxDispatch(); returnuseCallback((filter: VisibilityFilterType) =>{ dispatch({ type:'SET_VISIBILITY_FILTER', filter, }); }, [dispatch]); }; exportconstuseToggleTodo =()=>{ constdispatch = useReduxDispatch(); returnuseCallback((id:number) =>{ dispatch({ type:'TOGGLE_TODO', id, }); }, [dispatch]); };
以上实现其实并非真正意义上的动作生成器,而是返回动作分派器的 hook。
组件
我们并不在这里区分演示组件(presentational components)和容器组件(container components)。当然如何构造组件仍然是个值得探讨的话题,但是在本例中,组件都被视为扁平的。
./src/components/App.tsx:App 也和原始示例保持一致。
复制代码
import*asReactfrom'react'; importFooterfrom'./Footer'; importAddTodofrom'./AddTodo'; importVisibleTodoListfrom'./VisibleTodoList'; constApp: React.FC =()=>(); exportdefaultApp;
./src/components/Todo.tsx:这里做了一些小的修改,但没有特别大的改动。
复制代码
import*asReactfrom'react'; type Props = { onClick:(e: React.MouseEvent) =>void; completed: boolean; text: string; }; constTodo: React.FC =({ onClick, completed, text }) =>(
./src/components/VisibleTodoList.tsx:这里并未出现 mapStateToProps 或 selector 函数,只是在 render 中调用 getVisibleTodos。
复制代码
import*asReactfrom'react'; import{ useReduxState }from'reactive-react-redux'; import{ TodoType, State, VisibilityFilterType }from'../types'; import{ useToggleTodo }from'../actions'; importTodofrom'./Todo'; constgetVisibleTodos =(todos: TodoType[], filter: VisibilityFilterType) =>{ switch(filter) { case'SHOW_ALL': returntodos; case'SHOW_COMPLETED': returntodos.filter(t=>t.completed); case'SHOW_ACTIVE': returntodos.filter(t=>!t.completed); default: thrownewError(`Unknown filter:${filter}`); } }; constVisibleTodoList: React.FC =()=>{ conststate = useReduxState(); constvisibleTodos = getVisibleTodos(state.todos, state.visibilityFilter); consttoggleTodo = useToggleTodo(); return(
-
{visibleTodos.map(todo => (
toggleTodo(todo.id)} />
))}
./src/components/FilterLink.tsx:同样,当 useReduxState 函数返回整个 Redux 状态对象时,程序只是使用其属性对 active 进行评估。
复制代码
import*asReactfrom'react'; import{ useReduxState }from'reactive-react-redux'; import{ useSetVisibilityFilter }from'../actions'; import{ State, VisibilityFilterType }from'../types'; type Props = { filter: VisibilityFilterType; }; constFilterLink: React.FC =({ filter, children }) =>{ conststate = useReduxState(); constactive = filter === state.visibilityFilter; constsetVisibilityFilter = useSetVisibilityFilter(); return( ); }; exportdefaultFilterLink;
./src/components/Footer.tsx:由于有类型检查的保证,可以将字符串传递给 FilterLink 组件的 filter 属性。
复制代码
import*asReactfrom'react'; importFilterLinkfrom'./FilterLink'; constFooter: React.FC =()=>(Show: All Active Completed); exportdefaultFooter;
./src/components/AddTodo.tsx:这里对原始示例进行了一些修改,以便使用带有 useState 的受控表单。
复制代码
import*asReactfrom'react'; import{ useState }from'react'; import{ useAddTodo }from'../actions'; constAddTodo =()=>{ const[text, setText] = useState(''); constaddTodo = useAddTodo(); return(); }; export default AddTodo;
在线演示
请打开你的浏览器访问 codesandbox ,运行该示例。你也可以在 GitHub 上找到所有源代码。
其他信息
这篇文章中,并没有解释关于 reactive- response -redux 的内部细节。请访问 GitHub 查看更多信息。