七、Context
基本示例
import { createContext } from "react"; interface AppContextInterface { name: string; author: string; url: string; } const AppCtx = createContext<AppContextInterface | null>(null); // 应用程序中的提供程序 const sampleAppContext: AppContextInterface = { name: "Using React Context in a Typescript App", author: "thehappybug", url: "http://www.example.com", }; export const App = () => ( <AppCtx.Provider value={sampleAppContext}>...</AppCtx.Provider> ); // 在你的应用中使用 import { useContext } from "react"; export const PostInfo = () => { const appContext = useContext(AppCtx); return ( <div> Name: {appContext.name}, Author: {appContext.author}, Url:{" "} {appContext.url} </div> ); };
扩展示例使用createContext空对象作为默认值
interface ContextState { // 使用上下文设置你想要处理的状态类型,例如 name: string | null; } // 设置一个空对象为默认状态 const Context = createContext({} as ContextState); // 像在 JavaScript 中一样设置上下文提供程序
使用createContext 和 context getters来制作 a createCtx with no ,但无需检查:
import { createContext, useContext } from "react"; const currentUserContext = createContext<string | undefined>(undefined); function EnthusasticGreeting() { const currentUser = useContext(currentUserContext); return <div>HELLO {currentUser!.toUpperCase()}!</div>; } function App() { return ( <currentUserContext.Provider value="Anders"> <EnthusasticGreeting /> </currentUserContext.Provider> ); }
注意我们需要的显式类型参数,因为我们没有默认string值:
const currentUserContext = createContext<string | undefined>(undefined); // ^^^^^^^^^^^^^^^^^^
连同非空断言告诉 TypeScript currentUser肯定会在那里:
return <div>HELLO {currentUser!.toUpperCase()}!</div>; //
这是不幸的,因为我们知道稍后在我们的应用程序中,a Provider将填充上下文。
有几个解决方案:
1、可以通过断言非空来解决这个问题:
const currentUserContext = createContext<string>(undefined!);
2、我们可以编写一个名为的辅助函数createCtx来防止访问Context未提供值的 a。通过这样做,API 相反,我们不必提供默认值,也不必检查:
import { createContext, useContext } from "react"; /** * 创建上下文和提供者的助手,没有预先的默认值,并且 * 无需一直检查未定义。 */ function createCtx<A extends {} | null>() { const ctx = createContext<A | undefined>(undefined); function useCtx() { const c = useContext(ctx); if (c === undefined) throw new Error("useCtx must be inside a Provider with a value"); return c; } return [useCtx, ctx.Provider] as const; // 'as const' 使 TypeScript 推断出一个元组 } // 用法: // 我们仍然需要指定一个类型,但没有默认值! export const [useCurrentUserName, CurrentUserProvider] = createCtx<string>(); function EnthusasticGreeting() { const currentUser = useCurrentUserName(); return <div>HELLO {currentUser.toUpperCase()}!</div>; } function App() { return ( <CurrentUserProvider value="Anders"> <EnthusasticGreeting /> </CurrentUserProvider> ); }
3、可以更进一步,使用createContext和context getters结合这个想法。
import { createContext, useContext } from "react"; /** * 创建上下文和提供者的助手,没有预先的默认值,并且 * 无需一直检查未定义。 */ function createCtx<A extends {} | null>() { const ctx = createContext<A | undefined>(undefined); function useCtx() { const c = useContext(ctx); if (c === undefined) throw new Error("useCtx must be inside a Provider with a value"); return c; } return [useCtx, ctx.Provider] as const; // 'as const' 使 TypeScript 推断出一个元组 } // 用法 export const [useCtx, SettingProvider] = createCtx<string>(); // 指定类型,但不需要预先指定值 export function App() { const key = useCustomHook("key"); // 从钩子中获取值,必须在组件中 return ( <SettingProvider value={key}> <Component /> </SettingProvider> ); } export function Component() { const key = useCtx(); // 仍然可以在没有空检查的情况下使用! return <div>{key}</div>; }
4、使用createContext and useContext制作一个createCtx with unstated-like 上下文设置器:
import { createContext, Dispatch, PropsWithChildren, SetStateAction, useState, } from "react"; export function createCtx<A>(defaultValue: A) { type UpdateType = Dispatch<SetStateAction<typeof defaultValue>>; const defaultUpdate: UpdateType = () => defaultValue; const ctx = createContext({ state: defaultValue, update: defaultUpdate, }); function Provider(props: PropsWithChildren<{}>) { const [state, update] = useState(defaultValue); return <ctx.Provider value={{ state, update }} {...props} />; } return [ctx, Provider] as const; // 或者,[typeof ctx, typeof Provider] } // 用法 import { useContext } from "react"; const [ctx, TextProvider] = createCtx("someText"); export const TextContext = ctx; export function App() { return ( <TextProvider> <Component /> </TextProvider> ); } export function Component() { const { state, update } = useContext(TextContext); return ( <label> {state} <input type="text" onChange={(e) => update(e.target.value)} /> </label> ); }
八、forwardRef/createRef
检查Hooks 部分的useRef.
createRef:
import { createRef, PureComponent } from "react"; class CssThemeProvider extends PureComponent<Props> { private rootRef = createRef<HTMLDivElement>(); // 像这样 render() { return <div ref={this.rootRef}>{this.props.children}</div>; } }
forwardRef:
import { forwardRef, ReactNode } from "react"; interface Props { children?: ReactNode; type: "submit" | "button"; } export type Ref = HTMLButtonElement; export const FancyButton = forwardRef<Ref, Props>((props, ref) => ( <button ref={ref} className="MyClassName" type={props.type}> {props.children} </button> ));
通用 forwardRefs1 - Wrapper component
type ClickableListProps<T> = { items: T[]; onSelect: (item: T) => void; mRef?: React.Ref<HTMLUListElement> | null; }; export function ClickableList<T>(props: ClickableListProps<T>) { return ( <ul ref={props.mRef}> {props.items.map((item, i) => ( <li key={i}> <button onClick={(el) => props.onSelect(item)}>Select</button> {item} </li> ))} </ul> ); }
2 - Redeclare forwardRef
// 重新声明 forwardRef declare module "react" { function forwardRef<T, P = {}>( render: (props: P, ref: React.Ref<T>) => React.ReactElement | null ): (props: P & React.RefAttributes<T>) => React.ReactElement | null; } // 只需像以前一样编写组件! import { forwardRef, ForwardedRef } from "react"; interface ClickableListProps<T> { items: T[]; onSelect: (item: T) => void; } function ClickableListInner<T>( props: ClickableListProps<T>, ref: ForwardedRef<HTMLUListElement> ) { return ( <ul ref={ref}> {props.items.map((item, i) => ( <li key={i}> <button onClick={(el) => props.onSelect(item)}>Select</button> {item} </li> ))} </ul> ); } export const ClickableList = forwardRef(ClickableListInner);
九、有用的hooks
useLocalStorage
import { useState } from "react"; // 用法 function App() { // 类似于 useState 但第一个 arg 是本地存储中值的键。 const [name, setName] = useLocalStorage<string>("name", "Bob"); return ( <div> <input type="text" placeholder="Enter your name" value={name} onChange={(e) => setName(e.target.value)} /> </div> ); } // Hook function useLocalStorage<T>( key: string, initialValue: T ): [T, (value: T | ((val: T) => T)) => void] { // 状态来存储我们的值 // 将初始状态函数传递给 useState,因此逻辑只执行一次 const [storedValue, setStoredValue] = useState<T>(() => { try { // 按键从本地存储中获取 const item = window.localStorage.getItem(key); // 解析存储的 json 或者如果没有则返回 initialValue return item ? JSON.parse(item) : initialValue; } catch (error) { // 如果错误也返回initialValue console.log(error); return initialValue; } }); // 返回 useState 的 setter 函数的包装版本,它... // ... 将新值保存到 localStorage。 const setValue = (value: T | ((val: T) => T)) => { try { // 允许 value 是一个函数,所以我们有与 useState 相同的 API const valueToStore = value instanceof Function ? value(storedValue) : value; // 保存状态 setStoredValue(valueToStore); // 保存到本地存储 window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { // 更高级的实现将处理错误情况 console.log(error); } }; return [storedValue, setValue]; }
useMedia
import { useState, useEffect } from 'react'; function App() { const columnCount = useMedia<number>( // 媒体查询 ['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'], // 列数(与上述按数组索引的媒体查询有关) [5, 4, 3], // 默认列数 2 ); // 创建列高数组(从 0 开始) let columnHeights = new Array(columnCount).fill(0); // 创建包含每列项目的数组数组 let columns = new Array(columnCount).fill().map(() => []) as Array<DataProps[]>; (data as DataProps[]).forEach(item => { // 获取最短列的索引 const shortColumnIndex = columnHeights.indexOf(Math.min(...columnHeights)); // 添加项目 columns[shortColumnIndex].push(item); // 更新高度 columnHeights[shortColumnIndex] += item.height; }); // 渲染列和项目 return ( <div className="App"> <div className="columns is-mobile"> {columns.map(column => ( <div className="column"> {column.map(item => ( <div className="image-container" style={{ // 将图像容器大小调整为图像的纵横比 paddingTop: (item.height / item.width) * 100 + '%' }} > <img src={item.image} alt="" /> </div> ))} </div> ))} </div> </div> ); } // Hook const useMedia = <T>(queries: string[], values: T[], defaultValue: T) => { // 包含每个查询的媒体查询列表的数组 const mediaQueryLists = queries.map(q => window.matchMedia(q)); // 根据匹配的媒体查询获取值的函数 const getValue = () => { // 获取第一个匹配的媒体查询的索引 const index = mediaQueryLists.findIndex(mql => mql.matches); // 返回相关值,如果没有则返回默认值 return values?.[index] || defaultValue; }; // 匹配值的状态和设置器 const [value, setValue] = useState<T>(getValue); useEffect( () => { // 事件监听回调 // 注意:通过在 useEffect 之外定义 getValue,我们确保它具有 ... // ... 钩子参数的当前值(因为这个钩子回调在挂载时创建一次)。 const handler = () => setValue(getValue); // 使用上述处理程序为每个媒体查询设置一个侦听器作为回调。 mediaQueryLists.forEach(mql => mql.addListener(handler)); // 在清理时移除监听器 return () => mediaQueryLists.forEach(mql => mql.removeListener(handler)); }, [] // 空数组确保效果仅在挂载和卸载时运行 ); return value; }
useAsyncTask
// 用法 const task = useAsyncTask(async (data: any) => await myApiRequest(data)); task.run(data); useEffect(() => { console.log(task.status); // 'IDLE' | 'PROCESSING' | 'ERROR' | 'SUCCESS'; }, [task.status]); // 执行 import { useCallback, useState } from "react"; type TStatus = "IDLE" | "PROCESSING" | "ERROR" | "SUCCESS"; function useAsyncTask<T extends any[], R = any>( task: (...args: T) => Promise<R> ) { const [status, setStatus] = useState<TStatus>("IDLE"); const [message, setMessage] = useState(""); const run = useCallback(async (...arg: T) => { setStatus("PROCESSING"); try { const resp: R = await task(...arg); setStatus("SUCCESS"); return resp; } catch (error) { let message = error?.response?.data?.error?.message || error.message; setMessage(message); setStatus("ERROR"); throw error; } }, []); const reset = useCallback(() => { setMessage(""); setStatus("IDLE"); }, []); return { run, status, message, reset, }; } export default useAsyncTask;
useFetch
export function useFetch(request: RequestInfo, init?: RequestInit) { const [response, setResponse] = useState<null | Response>(null); const [error, setError] = useState<Error | null>(); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const abortController = new AbortController(); setIsLoading(true); (async () => { try { const response = await fetch(request, { ...init, signal: abortController.signal, }); setResponse(await response?.json()); setIsLoading(false); } catch (error) { if (isAbortError(error)) { return; } setError(error as any); setIsLoading(false); } })(); return () => { abortController.abort(); }; }, [init, request]); return { response, error, isLoading }; } // type guards function isAbortError(error: any): error is DOMException { if (error && error.name === "AbortError") { return true; } return false; }
十、HOC
一个 HOC 示例
注入props
interface WithThemeProps { primaryColor: string; }
在组件中的使用
在组件的接口上提供可用的props,但在包装在 HoC 中时为组件的消费者减去。
interface Props extends WithThemeProps { children?: React.ReactNode; } class MyButton extends React.Component<Props> { public render() { // 使用主题和其他props渲染元素。 } private someInternalMethod() { // 主题值也可在此处作为props使用。 } } export default withTheme(MyButton);
使用组件
现在,在使用组件时,可以省略primaryColor props或覆盖通过上下文提供的props。
<MyButton>Hello button</MyButton> // 有效 <MyButton primaryColor="#333">Hello Button</MyButton> // 同样有效
声明 HoC
实际的 HoC。
export function withTheme<T extends WithThemeProps = WithThemeProps>( WrappedComponent: React.ComponentType<T> ) { // 尝试为 React 开发工具创建一个不错的 displayName。 const displayName = WrappedComponent.displayName || WrappedComponent.name || "Component"; // 创建内部组件。这里计算出来的 Props 类型是魔法发生的地方。 const ComponentWithTheme = (props: Omit<T, keyof WithThemeProps>) => { // 获取要注入的props。这可以通过上下文来完成。 const themeProps = useTheme(); // props随后出现,因此可以覆盖默认值。 return <WrappedComponent {...themeProps} {...(props as T)} />; }; ComponentWithTheme.displayName = `withTheme(${displayName})`; return ComponentWithTheme; }
这是一个更高级的动态高阶组件示例,它的一些参数基于传入的组件的 props:
// 向组件注入静态值,以便始终提供它们 export function inject<TProps, TInjectedKeys extends keyof TProps>( Component: React.JSXElementConstructor<TProps>, injector: Pick<TProps, TInjectedKeys> ) { return function Injected(props: Omit<TProps, TInjectedKeys>) { return <Component {...(props as TProps)} {...injector} />; }; }
使用forwardRef对于“真正的”可重用性,还应该考虑为 HOC 公开一个 ref。
十一、Linting
yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint
将lint脚本添加到您的package.json:
"scripts": { "lint": "eslint 'src/**/*.ts'" },
一个合适的.eslintrc.js
module.exports = { env: { es6: true, node: true, jest: true, }, extends: "eslint:recommended", parser: "@typescript-eslint/parser", plugins: ["@typescript-eslint"], parserOptions: { ecmaVersion: 2017, sourceType: "module", }, rules: { indent: ["error", 2], "linebreak-style": ["error", "unix"], quotes: ["error", "single"], "no-console": "warn", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ "error", { vars: "all", args: "after-used", ignoreRestSiblings: false }, ], "@typescript-eslint/explicit-function-return-type": "warn", // 考虑对对象字面量和函数返回类型使用显式注释,即使它们可以被推断出来。 "no-empty": "warn", }, };
更多.eslintrc.json选项需要考虑,可能需要更多应用选项:
{ "extends": [ "airbnb", "prettier", "prettier/react", "plugin:prettier/recommended", "plugin:jest/recommended", "plugin:unicorn/recommended" ], "plugins": ["prettier", "jest", "unicorn"], "parserOptions": { "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "env": { "es6": true, "browser": true, "jest": true }, "settings": { "import/resolver": { "node": { "extensions": [".js", ".jsx", ".ts", ".tsx"] } } }, "overrides": [ { "files": ["**/*.ts", "**/*.tsx"], "parser": "typescript-eslint-parser", "rules": { "no-undef": "off" } } ] }
十二、最后
在我们阅读完官方文档后,我们一定会进行更深层次的学习,比如看下框架底层是如何运行的,以及源码的阅读。
这里广东靓仔给下一些小建议:
- 在看源码前,我们先去官方文档复习下框架设计理念、源码分层设计
- 阅读下框架官方开发人员写的相关文章
- 借助框架的调用栈来进行源码的阅读,通过这个执行流程,我们就完整的对源码进行了一个初步的了解
- 接下来再对源码执行过程中涉及的所有函数逻辑梳理一遍