使用ReduxHooks完成一个小实例

25次阅读

共计 8241 个字符,预计需要花费 21 分钟才能阅读完成。

React Hooks 是 React 在 16.8 版本出的新特性。在 16.8 以前,React 函数组件无法使用 state 状态、生命周期等功能,而有了 Hooks,就可以使用函数式编写和类一样强大的组件。

类组件有什么问题?

在以前使用一个类来封装一个组件是很正常的事,但是类比函数复杂,即使是一个很简单的组件,使用“类”来编写显得很重:

class Counter extends Component {constructor(props) {super(props);
        this.state = {num: 0};
    }
    addClick = (e) => {this.setState({ num: this.state.num + 1}); // 状态增 1
    }
    render() {
        return <div>
            <label>num:{this.state.num}</label>
            <button type="button" onClick={this.addClick}>add</button>
        </div>
    }
}

以上是一个简单的计数器组件,每点击 add 按钮状态会增加 1,只是这样一个简单的功能就会写很多代码,如果使用 Hooks 改写为函数组件:

function Counter(props) {let [num, setNum] = useState(0);
    return <div>
        <label>num:{num}</label>
        <button type="button" onClick={() => setNum(num + 1)}>add</button>
    </div>;
}

可以看到功能完全一样,运行效果:

当组件越来越复杂类组件扩展功能首选是使用“高阶组件”,这是造成代码晦涩难懂的根源,每当增加一个逻辑组件都会外套一层,而 Hooks 会解决此类问题,让开发者从中解脱出来。

Hooks 意思是钩子,React 的意思是将组件的特性使用“钩子”从外界钩进来,力求达到类组件一样丰富的功能,让我们以函数式的思想来编写组件。

React 提供了很多现成的 HooksAPI,简单说两个一会儿用到的:

useState

useState 是 React 提供可以在函数组件使用状态的钩子,旧版的 React 中,函数组件只能开发一些显示内容的简单功能,要想使用 state 必须切回类组件中去。useState 接收一个初始值,它返回一个数组,元素 1 是状态对象,2 是一个更新状态的函数:

let [num, setNum] = useState(0);

返回数组是 React 让我们更方便的重命名,我们直接解构即可,这样每调用 setNum(新值) 即可更新状态 num,非常方便。

useRef

React 是基于组件的技术,我们在类组件中要想直接操作 DOM 则是通过 ref 引用(使用React.createRef()),而 useRef 钩子是帮助我们创建 ref 对象:

let inputRef = useRef(null);

同样 useRef 接收一个初始值,它用来初始化对象的 current 属性,注意不是 current.value,这样在相应的 DOM 元素上将对象赋给 ref 属性即可:

<input type="text" id="name" ref={inputRef} />

想要取元素的内容通过 value 属性:

console.log(inputRef.current.value); // 输出文本框的值

下面使用 Hooks 结合 Redux 编写一个小项目

使用 Redux+Hooks 完成一个 TODO 实例

在以前刚刚学习 Redux 的时候我写了一个 TODO 待办的小功能,目的就是熟悉一下 Redux 的使用,而 Hooks 新特性推出了之后为了掌握 Hooks 与 Redux,这次写一个 Hooks 的版本,功能与之前完全一至:

  • 可以让用户添加待办事项(todo)
  • 可以统计出还有多少项没有完成
  • 用户可以勾选某 todo 置为已完成
  • 可筛选查看条件(显示全部、显示已完成、显示未完成)

目录结构:

src
┗━ components 存放组件
    ┗━ TodoHeader.jsx
    ┗━ TodoList.jsx
    ┗━ TodoFooter.jsx
┗━ store 保存 redux 的相关文件
    ┗━ action
        ┗━ types.js 定义动作类型
    ┗━ reducer
        ┗━ index.js 定义 reducer
    ┗━ index.js 默认文件用于导出 store

组件分为 3 个:

  • TodoHeader 用于展示未办数量
  • TodoList 按条件展示待办项列表及添加待办
  • TodoFooter 功能按钮(显示全部、未完成、已完成)

在从头开始时,我们先要定义好初始的状态,在 reducer 目录中新建 index.js 文件,定义好初始的 state 数据:

let initState = {
    todos: [
        {id: ~~(Math.random() * 1000000),
            title: '学习 React',
            isComplete: true
        },
        {id: ~~(Math.random() * 1000000),
            title: '学习 Node',
            isComplete: false
        },
        {id: ~~(Math.random() * 1000000),
            title: '学习 Hooks',
            isComplete: false
        }
    ]
};

function reducer(state = initState, action) {}

export default reducer;

以上我们手写 3 条来模拟初始数据,把它们存放到 todos 的数组中。接下来创建一个空的 reducer 方法传入初始数据,这样就可以基于旧的 state 更新出新的对象。

写好 reducer 方法后,我们接下来创建 redux 仓库,在 store 目录中也新建一个 index.js,引入刚刚写好的 reducer,即可创建出仓库对象:

import {createStore} from 'redux';
import reducer from './reducer';
let store = createStore(reducer); // 传入 reducer
export default store; // 导出仓库

这样准备工作已经完成。

统计未完成的事项

接下来完成第一个功能,统计出 TODO 列表中所有未完成的数量。首先,我们定义一个头组件 TodoHeader.jsx:

import React from 'react';

function TodoHeader(props) {let state = useSelector((state) => ({todos: state.todos}));
    return <div></div>;
}

export default TodoHeader;

可以看到,使用 Hooks 我们的组件一律使用函数写法,目前此组件还没有任何功能,我们先导出它给顶层组件用于注入 Redux 仓库。

然后在顶层组件中使用 react-redux 库提供的 Provider 组件注入 store:

import React from 'react';
import ReactDOM from 'react-dom';
import TodoHeader from './components/TodoHeader.jsx';
import {Provider} from 'react-redux';
import store from './store';

function Index(props) {
    return <>
        <Provider store={store}>
            <TodoHeader />
        </Provider>
    </>;
}

ReactDOM.render(<Index />, document.querySelector('#root'));

可以看到,此时此刻和类组件的开发方式没有任何区别,接下来的工作就是要在 TodoHeader.jsx 组件拿到仓库数据,来编写统计功能。

在使用 Hooks 开发时,关联 Redux 仓库不再使用 connect 高阶函数来实现,react-redux包为我们提供了一个自定义钩子:useSelector

它的功能与高阶函数 connect 类似,它接收两个函数,其中第一个函数的功能就是将返回值作为 useSelector 的返回值,并自动处理好订阅,当派发动作时会自动触发组件的渲染:

let state = useSelector((state) => ({todos: state.todos}));    

以上返回了 state.todos,这样就可以在组件中拿到初始化的 3 条 todos 数据。下面即可编写逻辑,统计出所有未完成的数量:

function TodoHeader(props) {let state = useSelector((state) => ({todos: state.todos}));
    /**
     * 统计未完成数量
     */
    function getUncompleteCount(todos) {return todos.filter(item => !item.isComplete).length;
    }
    return <div> 您有 {getUncompleteCount(state.todos)} 项 TODO 未完成 </div>
}

多说几句,useSelector 与 connect 有几处不同,userSelector 可以返回任意值不仅仅是对象;而且它可以多次调用。当动作派发的时候,useSelector 会将当前的结果值与上一次进行比较(使用严格相等===),如果相比不同,则会触发组件的渲染。

当一个组件中多次使用了 useSelector,为了提高效率,react-redux 将多次的 useSelect 作为批量更新,只会渲染1次。

展示待办列表

接下来完成展示待办列表的功能,新建一个 TodoList.jsx 组件,同样使用 useSelector 获取仓库数据:

function TodoList(props) {let state = useSelector((state) => state);
    // 其它代码略...
}

然后通过循环将仓库中的 todos 数据渲染到页面上,这里抽出一个方法来实现:

    /**
     * 渲染 Todo 列表
     */
    function renderList(todos) {return todos.map((item, index) => {if (item.isComplete) {return <li key={index}>
                    <input type="checkbox" data-id={item.id} checked={true} />
                    <del>{item.title}</del>
                </li>;
            } else {return <li key={index} data-id={item.id}>
                    <input type="checkbox" data-id={item.id} checked={false} />
                    <span>{item.title}</span>
                </li>;
            }
        });
    }

返回此函数结果即可完成:

function TodoList(props) {let state = useSelector((state) => state);
    // 其它代码略...
    return <div>
        <ul>
            {renderList(state.todos)}
        </ul>
    </div>
}

更新待办状态

下面完成更新待办项状态的功能,当用户给一条待办打勾,就将这条数据的 isComplete 属性置为true,标记为已完成。

由于有了用户的操作,我们需要编写动作 Action,我们在 action 目录下新建一个 types.js,用于存放动作类型:

// 更新完成状态
export const TOGGLE_COMPLETE = 'TOGGLE_COMPLETE';

以上就定义好了一个动作类型,可以看到非常简单,就是一个描述 Action 的字符串指令。

接下来为 checkbox 添加事件,当用户勾选了某一条待办,将记录的 id 值传给 reducer 来作更新:

<input type="checkbox" 
    data-id={item.id} 
    checked={true} 
    onChange={itemChange} />
function TodoList(props) {let dispatch = useDispatch(); // 取得派发方法
    /**
     * Todo 勾选事件
     */
    function itemChange(e) {const { target} = e;
        // 派发 TOGGLE_COMPLETE 动作以更新仓库
        dispatch({
            type: TOGGLE_COMPLETE, payload: {
                id: target.dataset.id, // 取得当前 id 值
                isComplete: target.checked
            }
        });
    }
    // 其它代码略...
}

以上使用了 react-reudx 库提供的第 2 个勾子方法:useDispatch

在使用 Redux 时,更新仓库的唯一办法就是使用派发方法 dispatch 来派发一个动作,在使用 Hooks 开发组件,useDispatch返回一个 Redux 库的 dispatch 方法引用,使用它与之前类组件通过 connect 的方式完全一致。

接下来就是在 reducer 中处理更新逻辑:

function reducer(state = initState, action) {
    let nextState = null;
    switch (action.type) {
        case TOGGLE_COMPLETE:
            nextState = {
                ...state,
                todos: state.todos.map((item) => {
                    // 将仓库中 id 为 payload.id 的那一条记录更新
                    if (item.id == action.payload.id) {
                        return { 
                            ...item, 
                            isComplete: action.payload.isComplete 
                        };
                    } else {return item;}
                })
            };
            break;
            // 其它代码略...
        default:
            nextState = state;
            break;
    }
    return nextState;
}

以上通过一个 TOGGLE_COMPLETE 分支来判断是不是“更新待办状态”这个动作,然后找到参数中传来的 id,将对应的记录更新即可。

使用 Hooks 要时刻记住 reducer 是一个纯函数,一定要保证每一次返回的结果都是一个新对象,因此 todos 的更新不要使用 slice 来直接修改(引用地址不变)。

添加待办

添加待办要求用户在一个文本框中输入内容,将数据添加到仓库中。

还是一样的套路,在 type.js 中新增一个动作类型,用于描述添加待办:

// 添加 TODO
export const ADD_TODO = 'ADD_TODO';

我们在 TodoHeader.jsx 组件中增加一个输入域,用于接收用户输入的内容:

function TodoHeader(props) {let newTodoInput = useRef(null); // 创建 ref 对象
    /**
     * 添加按钮事件
     */
    function addClick(e) {// 略...}
    // 其它代码略...

    return <div>
        <div> 您有 {getUncompleteCount(state.todos)} 项 TODO 未完成 </div>
        <div>
            {/* 将 ref 对象绑定到元素中 */}
            <input type="text" ref={newTodoInput} />
            <button type="button" onClick={addClick}> 添加 </button>
        </div>
    </div>
}

以上使用了 React 为我们提供了另一个钩子方法:useRef,使用它来创建一个 ref 对象将它绑定到对应的 DOM 元素上,即可以取得真实的 DOM 结点。这样我们就可以方便的拿到用户输入的内容:

    function addClick(e) {
        //current 即真实的 input 结点,value 即输入域的值
        let title = newTodoInput.current.value;
        dispatch({
            type: ADD_TODO, payload: {id: ~~(Math.random() * 1000000),
                title,
                isComplete: false
            }
        });
    }

接着还是派发对应的 ADD_TODO 动作即可,传入用户输入的内容,并生成一个新 id。

在 reducer 中再增加一处逻辑分支,用于处理“添加待办”:

function reducer(state = initState, action) {
    let nextState = null;
    switch (action.type) {
        case ADD_TODO:
            // 将新记录增加到仓库中
            nextState = {
                ...state,
                todos: [...state.todos, action.payload]
            };
            break;
        // 其它代码略...
        default:
            nextState = state;
            break;
    }
    return nextState;
}

仍要注意返回的对象要是个新的,到此添加功能已经完成。

删除功能非常简单,不在多说。

筛选查看条件

最后一个功能,根据用户指定的条件来过滤数据的显示。我们修改一下仓库的初始值,增加一个“显示条件”:

let initState = {
    display: 'all', //display 用于控制显示内容
    todos: [
        {id: ~~(Math.random() * 1000000),
            title: '学习 React',
            isComplete: true
        },
        // 略...
    ]
}

display 用于控制数据显示的内容,它只有 3 个值:已完成(complete)、未完成(uncomplete)和显示全部(all),我们默认定义为显示全部:“all”。

仍然是先定义好动作类型:

// 筛选查看
export const FILTER_DISPLAY = 'FILTER_DISPLAY';

新建一个 TodoFooter.jsx 组件,放入 3 个按钮,分别对应 3 个筛选条件:

function TodoFooter(props) {const dispatch = useDispatch();

    /**
     * 筛选查看事件(dispaly 为 all,complete,uncomplete3 个值)
     */
    function displayClick(display) {dispatch({ type: FILTER_DISPLAY, payload: display});
    }

    return <div>
        <hr />
        <button 
            type="button"
            onClick={() => displayClick('all')}> 显示全部 </button>
        <button 
            type="button"
            onClick={() => displayClick('complete')}> 已完成 </button>
        <button 
            type="button"
            onClick={() => displayClick('uncomplete')}> 未完成 </button>
    </div>
}

可以看到,这次抽出一个方法 displayClick 用于处理 3 个按钮对应的“条件”,将全部、已完成和未完成作为参数传入事件函数,派发到仓库即可。

接下来的工作就是再增加一个 reducer 分支,更新仓库中的 display 即可:

function reducer(state = initState, action) {
    let nextState = null;
    switch (action.type) {
        case FILTER_DISPLAY:
            nextState = {
                ...state,
                // 将仓库中的 display 条件更新
                display: action.payload
            };
            break;
        // 其它代码略...
        default:
            nextState = state;
            break;
    }
    return nextState;
}

最后一步,在渲染 TODO 列表时,根据仓库的 display 条件渲染即可:

    function renderList(todos, display) {
        return todos.filter(item => {
               // 根据 display 的分类来返回不同的数据
            switch (display) {
                case 'complete':
                    return !item.isComplete;
                case 'uncomplete':
                    return item.isComplete;
                default:
                    return true;
            }
        }).map((item, index) => {// 略...});
    }

到此,一个 ReduxHooks 版本的 TODO 小应用已实现完毕。

运行效果:

可以看到,使用 Hooks 开发组件更加的优雅,也是 React 未来的趋势。

正文完
 0