美文网首页
React Hooks的花样玩法

React Hooks的花样玩法

作者: SCQ000 | 来源:发表于2020-05-18 17:40 被阅读0次

    React Hooks是react 最新的编程范式,我们可以容易地写出更加简单和可扩展的代码。最近看了jsconf(https://www.youtube.com/watch?v=J-g9ZJha8FE)的会议分享后,觉得有很多代码实现思路都可以在自己的项目中借鉴,所以根据自己的理解对其主要内容做了一次总结。

    useDark

    对于做移动端的前端来说,换肤一般是比较常见的一个需求。在之前我们可能需要在redux中定义一个全局状态进行管理,现在利用React Hooks,就能很方便地实现这个功能了:

    function App() {
      const [isDark, setIsDark] = React.useState(false);
      
      const theme = isDark ? themes.dark : themes.light;
      
      return (
        <ThemeProvider theme={theme}>
            ...
        </ThemeProvider>
      );
    }
    

    可以看到,我们利用themes对象就可以控制前端UI显示的是黑夜模式还是日常模式。在移动端,通常是根据用户定义的系统主题颜色来判断UI显示的主题。那么我们如何实现这个功能呢?

    如果大家做过响应式应用开发,那么对媒体查询应该并不陌生。一般来说都会使用css来写媒体查询语句,不过在这里我们将使用matchMedia这个API来实现。它的功能主要是用来判断媒体查询语句在特定浏览器上是否生效,

    如:

    window.matchMedia('screen and (min-width: 800px)');
    

    这个命令就会判断浏览器的屏幕宽度是否大于800px。如果是的话,就会返回true,否则返回false。

    那么,我们就可以借助这个方法再结合prefers-color-scheme标志来判断用户设置了什么样的系统主题色。

    有了上述的知识后,再结合前面的ThemeProvider组件,我们就可以写出下面的代码来:

    const matchDark = '(prefers-color-scheme: dark)';
    
    function App() {
        const [isDark, setIsDark] = React.useState(() => window.matchMedia && window.matchMedia(matchDark).matches);
    
        React.useEffect(() => {
            const matcher = window.matchMedia(matchDark);
            const onChange = ({ matches }) => setIsDark(matches);
        
            matcher.addListener(onChange);
        
            return () => {
                matcher.removeListener(onChange);
            }
        }, [setIsDark]);
    
        const theme = isDark ? themes.dark : themes.light;
    
        return <ThemeProvider theme={theme}>...</ThemeProvider>
    }
    

    接下来,我们简化一下代码,将设置主题相关的代码抽取成自定义Hook:

    function useDarkMode() {
        const [isDark, setIsDark] = React.useState(() => window.matchMedia && window.matchMedia(matchDark).matches);
    
        React.useEffect(() => {
            const matcher = window.matchMedia(matchDark);
            const onChange = ({ matches }) => setIsDark(matches);
        
            matcher.addListener(onChange);
        
            return () => {
                matcher.removeListener(onChange);
            }
        }, [setIsDark]);
    
        return isDark;
    }
    
    function App() {
        const theme = useDarkMode() ? themes.dark : themes.light;
    
        return (<ThemeProvider theme={theme}>
            ...
        </ThemeProvider>)
    }
    

    useClickOutside

    模态框Modal是一种十分常见的前端组件,无论你是做菜单、弹窗还是提示框,这个功能都是必备的。那么在开发中,我们通常都会实现一个叫做“点击页面其他元素,modal自动关闭”的功能。

    现在利用React Hooks的useRef方法就可以实现这个功能了。useRef这个hook主要用来解决元素或组件引用的问题,我们可以通过给组件传入ref属性来获取当前组件的实例。

    实现原理比较简单,在document元素上绑定一个点击事件,判断当前点击元素是否是目标元素即可。封装成useClickOutside hook后,代码如下:

    function useClickOutside(elRef, callback) {
        const callbackRef = React.useRef();
        callbackRef.current = callback;
    
        React.useEffect(() => {
            const handleClickOutside = e => {
                if (elRef?.current?.contains(e.target) && callback) {
                    callbackRef.current(e);
                }
            }
    
            document.addEventListener('click', handleClickOutside, true);
    
            retrun () => {
                document.removeEventListener('click', handleClickOutside, true)
            }
        }, [callbackRef, elRef]);
    }
    

    有了这个自定义Hook后,传入所要使用的元素实例以及对应的回调函数即可:

    function Menu() {
        const menuRef = React.useRef();
    
        const onClickOutside = () => {
            console.log('clicked outside');
        };
    
        useClickOutside(menuRef, onClickOutside);
    
        return (<div ref={menuRef}></div>)
    }
    

    useSelector

    我们都知道,之前使用redux进行状态管理的时候,都需要用connect来封装组件。而react-redux从7.1之后发布了新的Hook API useSelector。利用它我们就可以替换原来需要用connect进行封装的高阶组件了:

    import { useSelector } from "react-redux";
    import { createSelector } from 'reselect';
    
    const selectHaveDoneTodos = createSelector(
        state => state.todos,
        todos => todos.filter(todo => todo.isDone)
    )
    
    function Todos() {
        const doneTodos = useSelector(selectHaveDoneTodos);
        return <div>{doneTodos}</div>
    }
    

    这样一来,就避免了代码中class组件和functional组件分散得到处都是的问题。

    全局状态管理

    对于全局状态的管理,我们可以结合createContextuseReducer来实现。前者会创建一个新的上下文对象,然后利用这个对象就可以保存一些特定的全局状态。而后者主要负责状态的分发和修改。

    下面来实现一个StoreProvider组件:

    const context = React.createContext();
    
    export function StoreProvider({
        children,
        reducer,
        initialState = {}
    }) {
        const [store, dispatch] = React.useReducer(reducer, initialState);
    
        const contextValue = React.useMemo(() => [store, dispatch], [store, dispatch]);
    
        return (<context.Provider value={contextValue}>
            {children}
        </context.Provider>)
    }
    

    可以看到该组件和react-redux提供的Provider组件类似,任何它的子组件都能够访问到对应的全局状态。如果你的应用比较简单,该组件完全就可以满足你的需要,不必再引入繁重的react-redux框架。

    多个上下文

    上面的组件并没有对外开放接口,所有

    const storeContext = React.createContext();
    const dispatchContext = React.createContext();
    
    export const StoreProvider = ({ children, reducer, initialState = {} }) => {
        const [store, dispatch] = React.useReducer(reducer, initialState);
    
        return (
            <dispatchContext.Provider value={dispatch}>
                <storeContext.Provider value={store}>
                    {childern}
                </storeContext.Provider>
            </dispatchContext.Provider>
        )
    }
    
    export function useStore() {
        return React.useContext(storeContext);
    }
    
    export function useDispatch() {
        return React.useContext(dispatchContext);
    }
    

    完成上面的基础工作后,我们再来看一下,要如何在组件中更新状态呢?

    import { useDispatch } from "./useStore";
    
    function Todo ({ todo }) {
        const dispatch = useDispatch();
    
        const handleClick = () => {
            dispatch({ type: 'toggleTodo', todoId: todo.id });
        }
    
        return (
            <div onClick={handleClick}>{todo.name}</div>
        )
    }
    

    可以看到,组件状态的更新主要是利用useStore暴露出来的dispatch方法来实现,核心思想和redux是类似的,都是通过单一数据流。

    我们同样可以借鉴redux的思想,来实现一个工厂方法:

    function makeStore(reducer, initialState) {
        // do something
        return [StoreProvider, useDispatch, useStore];
    }
    

    利用makeStore这个方法,只要传入初始状态和reducer就能实现自定义的状态管理器:

    import makeStore from './makeStore'
    
    const todosReducer = (state, action) => {...}
    
    const [
        TodosProvider,
        useTodos,
        useTodosDispatch
    ] = makeStore(todosReducer, [])
    
    export { TodosProvider, useTodos, useTodosDispatch }
    

    从缓存中恢复状态

    有时候为了提供应用的性能,你需要利用缓存技术。那么我们完全可以借助localStorage来给状态加上持久化的功能。只要在每次更新状态的时候,同时更新localStorage里的值,然后下次再创建store时就能自动获取缓存,从而加快应用的启动。

    export default function makeStore(userReducer, initialState, key) {
        const dispatchContext = React.createContext();
        const storeContext = React.createContext();
    
        try {
            initialState = JSON.parse(localStorage.getItem(key)) || initailState
        } catch {}
    
        const reducer = (state, action) => {
            const newState = userReducer(state, action);
            localStorage.setItem(key, JSON.stringify(newState));
            return newState;
        }
    
        const StoreProvider = ({ childern }) => {
            const [store, dispatch] = React.useReducer(reducer, initialState);
    
            return (
                <dispatchContext.Provider value={dispatch}>
                    //...
                </dispatchContext.Provider>
            )
        }
    }
    

    异步处理

    用户界面通常是同步的,而业务逻辑,如状态、计算等等通常是异步的,那么如何处理这些逻辑呢?

    我们可以先创建一个自定义hook:useTodos,它会返回异步请求对应的数据以及状态:

    import { useTodosStore } from "./useTodosStore";
    
    export function useTodos() {
        // do something
    
        return {
            todos,
            isLoading: false,
            error: null
        }
    }
    

    接着我们利用useState,useEffectaxios来扩充一下功能:

    export function useTodos() {
        const [todos, setTodos] = React.useState({});
        const [isLoading, setIsLoading] = React.useState(false);
        const [error, setError] = React.useState(null)
    
        const fetchTodos = React.useCallback(async () => {
            setIsLoading(true)
    
            try {
                const {data: todos} = await axios.get('/todos');
                setTodos(todos)
            } catch (err) {
                setError(err)
            }
    
            setIsLoading(false)
        }, [setIsLoading, setTodos, setError]);
    
        React.useEffect(() => {
            fetchTodos()
        }, [fetchTodos]);
    
        return {
            todos,
            isLoading,
            error
        }
    }
    

    我们可以进一步简化这部分的代码,将公用的数据请求逻辑抽取出来,成为usePromise hook:

    function usePromise(callback) {
        const [isLoading, setIsLoading] = React.useState(false);
        const [error, setError] = React.useState(null);
        const [data, setData] = React.useState(null);
        
        const process = async () => {
            setIsLoading(true);
    
            try {
                const data = await callback();
                setData(data);
            } catch (err) {
                setError(err);
            }
    
            setIsLoading(false)
        };
    
        React.useEffect(() => {
            process();
        }, [setIsLoading, setData, setError]);
    
        return {
            data,
            isLoading,
            error
        }
    }
    
    export function useTodos() {
        const getTodos = React.useCallback(async () => {
            const { data } = await axios.get('/todos');
            return data;
        }, []);
    
        const { data: todos, isLoading, error } = usePromise(getTodos);
    
        return {
            todos,
            isLoading,
            error
        }
    }
    

    完成后,我们就可以在组件中使用这部分代码了。

    总结

    自从react hooks发布以来,以前很多冗余的状态逻辑处理都能很轻松地进行抽象复用。大家也可以在github等地方找到别人实现的许多自定义hooks,利用这些自定义hooks可以让我们前端的代码更加简洁和优雅。最后,推荐一个网站,https://usehooks.com/ 这个网站上记录了很多实用的hooks,大家可以按需使用。

    ——--转载请注明出处--———

    最后,欢迎大家关注我的公众号,一起学习交流。


    微信扫描二维码,关注我的公众号.jpg

    参考资料

    https://www.youtube.com/watch?v=J-g9ZJha8FE

    https://learning.oreilly.com/library/view/the-modern-web/9781457172489/media_queries_in_javascript.html
    https://www.30secondsofcode.org/react/s/use-click-outside

    https://kentcdodds.com/blog/how-to-use-react-context-effectively/

    相关文章

      网友评论

          本文标题:React Hooks的花样玩法

          本文链接:https://www.haomeiwen.com/subject/otqlohtx.html