React Hooks的花樣玩法

React Hooks是react 最新的編程范式,我們可以容易地寫出更加簡單和可擴(kuò)展的代碼。最近看了jsconf(https://www.youtube.com/watch?v=J-g9ZJha8FE)的會議分享后,覺得有很多代碼實現(xiàn)思路都可以在自己的項目中借鑒,所以根據(jù)自己的理解對其主要內(nèi)容做了一次總結(jié)。

useDark

對于做移動端的前端來說,換膚一般是比較常見的一個需求。在之前我們可能需要在redux中定義一個全局狀態(tài)進(jìn)行管理,現(xiàn)在利用React Hooks,就能很方便地實現(xiàn)這個功能了:

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

可以看到,我們利用themes對象就可以控制前端UI顯示的是黑夜模式還是日常模式。在移動端,通常是根據(jù)用戶定義的系統(tǒng)主題顏色來判斷UI顯示的主題。那么我們?nèi)绾螌崿F(xiàn)這個功能呢?

如果大家做過響應(yīng)式應(yīng)用開發(fā),那么對媒體查詢應(yīng)該并不陌生。一般來說都會使用css來寫媒體查詢語句,不過在這里我們將使用matchMedia這個API來實現(xiàn)。它的功能主要是用來判斷媒體查詢語句在特定瀏覽器上是否生效,

如:

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

這個命令就會判斷瀏覽器的屏幕寬度是否大于800px。如果是的話,就會返回true,否則返回false。

那么,我們就可以借助這個方法再結(jié)合prefers-color-scheme標(biāo)志來判斷用戶設(shè)置了什么樣的系統(tǒng)主題色。

有了上述的知識后,再結(jié)合前面的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>
}

接下來,我們簡化一下代碼,將設(shè)置主題相關(guān)的代碼抽取成自定義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

模態(tài)框Modal是一種十分常見的前端組件,無論你是做菜單、彈窗還是提示框,這個功能都是必備的。那么在開發(fā)中,我們通常都會實現(xiàn)一個叫做“點擊頁面其他元素,modal自動關(guān)閉”的功能。

現(xiàn)在利用React Hooks的useRef方法就可以實現(xiàn)這個功能了。useRef這個hook主要用來解決元素或組件引用的問題,我們可以通過給組件傳入ref屬性來獲取當(dāng)前組件的實例。

實現(xiàn)原理比較簡單,在document元素上綁定一個點擊事件,判斷當(dāng)前點擊元素是否是目標(biāo)元素即可。封裝成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后,傳入所要使用的元素實例以及對應(yīng)的回調(diào)函數(shù)即可:

function Menu() {
    const menuRef = React.useRef();

    const onClickOutside = () => {
        console.log('clicked outside');
    };

    useClickOutside(menuRef, onClickOutside);

    return (<div ref={menuRef}></div>)
}

useSelector

我們都知道,之前使用redux進(jìn)行狀態(tài)管理的時候,都需要用connect來封裝組件。而react-redux從7.1之后發(fā)布了新的Hook API useSelector。利用它我們就可以替換原來需要用connect進(jìn)行封裝的高階組件了:

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組件分散得到處都是的問題。

全局狀態(tài)管理

對于全局狀態(tài)的管理,我們可以結(jié)合createContextuseReducer來實現(xiàn)。前者會創(chuàng)建一個新的上下文對象,然后利用這個對象就可以保存一些特定的全局狀態(tài)。而后者主要負(fù)責(zé)狀態(tài)的分發(fā)和修改。

下面來實現(xiàn)一個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組件類似,任何它的子組件都能夠訪問到對應(yīng)的全局狀態(tài)。如果你的應(yīng)用比較簡單,該組件完全就可以滿足你的需要,不必再引入繁重的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);
}

完成上面的基礎(chǔ)工作后,我們再來看一下,要如何在組件中更新狀態(tài)呢?

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

可以看到,組件狀態(tài)的更新主要是利用useStore暴露出來的dispatch方法來實現(xiàn),核心思想和redux是類似的,都是通過單一數(shù)據(jù)流。

我們同樣可以借鑒redux的思想,來實現(xiàn)一個工廠方法:

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

利用makeStore這個方法,只要傳入初始狀態(tài)和reducer就能實現(xiàn)自定義的狀態(tài)管理器:

import makeStore from './makeStore'

const todosReducer = (state, action) => {...}

const [
    TodosProvider,
    useTodos,
    useTodosDispatch
] = makeStore(todosReducer, [])

export { TodosProvider, useTodos, useTodosDispatch }

從緩存中恢復(fù)狀態(tài)

有時候為了提供應(yīng)用的性能,你需要利用緩存技術(shù)。那么我們完全可以借助localStorage來給狀態(tài)加上持久化的功能。只要在每次更新狀態(tài)的時候,同時更新localStorage里的值,然后下次再創(chuàng)建store時就能自動獲取緩存,從而加快應(yīng)用的啟動。

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

異步處理

用戶界面通常是同步的,而業(yè)務(wù)邏輯,如狀態(tài)、計算等等通常是異步的,那么如何處理這些邏輯呢?

我們可以先創(chuàng)建一個自定義hook:useTodos,它會返回異步請求對應(yīng)的數(shù)據(jù)以及狀態(tài):

import { useTodosStore } from "./useTodosStore";

export function useTodos() {
    // do something

    return {
        todos,
        isLoading: false,
        error: null
    }
}

接著我們利用useState,useEffectaxios來擴(kuò)充一下功能:

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

我們可以進(jìn)一步簡化這部分的代碼,將公用的數(shù)據(jù)請求邏輯抽取出來,成為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
    }
}

完成后,我們就可以在組件中使用這部分代碼了。

總結(jié)

自從react hooks發(fā)布以來,以前很多冗余的狀態(tài)邏輯處理都能很輕松地進(jìn)行抽象復(fù)用。大家也可以在github等地方找到別人實現(xiàn)的許多自定義hooks,利用這些自定義hooks可以讓我們前端的代碼更加簡潔和優(yōu)雅。最后,推薦一個網(wǎng)站,https://usehooks.com/ 這個網(wǎng)站上記錄了很多實用的hooks,大家可以按需使用。

——--轉(zhuǎn)載請注明出處--———

最后,歡迎大家關(guān)注我的公眾號,一起學(xué)習(xí)交流。


微信掃描二維碼,關(guān)注我的公眾號.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/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容