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

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 
} }

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

function Counter(props) {
    let [num, setNum] = useState(0);
    return 
; }

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

当组件越来越复杂类组件扩展功能首选是使用“高阶组件”,这是造成代码晦涩难懂的根源,每当增加一个逻辑组件都会外套一层,而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属性即可:

想要取元素的内容通过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 
; } 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 <>
        
            
        
    ;
}

ReactDOM.render(, 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 
您有{getUncompleteCount(state.todos)}项TODO未完成
}

多说几句,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 
  • {item.title}
  • ; } else { return
  • {item.title}
  • ; } }); }

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

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

    更新待办状态

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

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

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

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

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

    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 
    您有{getUncompleteCount(state.todos)}项TODO未完成
    {/* 将ref对象绑定到元素中 */}
    }

    以上使用了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 

    }

    可以看到,这次抽出一个方法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未来的趋势。

    你可能感兴趣的:(redux,hooks,react.js)