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組件分散得到處都是的問題。
全局狀態管理
對於全局狀態的管理,我們可以結合createContext
和useReducer
來實現。前者會創建一個新的上下文對象,然後利用這個對象就可以保存一些特定的全局狀態。而後者主要負責狀態的分發和修改。
下面來實現一個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
,useEffect
和axios
來擴充一下功能:
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,大家可以按需使用。
——--轉載請註明出處--———
最後,歡迎大家關注我的公衆號,一起學習交流。
參考資料
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/