学习并使用 2021 年最酷的前端技术栈。
Next.js
+ Chakra-ui
+ Recoil
+ axios-hooks
+ ...
- v0.1.0 : 初始 next.js 框架 (
typescript
) - v0.1.1 : 添加
prettier
配置, 添加 layout 组件,添加Static Generation
示例 - v0.1.2 : 添加 chakra-ui 框架,recoil 状态管理库
- v0.1.3 : 添加了 axios-hooks 用于数据请求
- v0.1.4 : 重构了页面目录,增加了一个 React.momo 示例,一个 framer-motion 示例
- v0.1.5 : 扩展
next.js
实现了i18n
功能 - v0.1.6 : 添加了 immer 状态管理库
- v0.1.7 :
- 增加了
ReactTable
组件和示例 - 增加了
useImperativeHane
文档和示例 - 增加了
@chakra-ui/icons
依赖 component
文件夹重命名为buildin-components
next.js
更新到11.1.0
- 修复静态导出模式时
i18n
报错
- 增加了
- v0.1.8 :
- 更改内置组件目录到
builtin
- 添加了
useDebounce
、useInterval
、useTimeout
、useEventListener
hooks - 添加了
HolyGrail/SideBar
布局示例 - 添加了 自定义
scrollbar
样式示例 - 所有依赖更新到
latest
- 更改内置组件目录到
TODO:
持续丰富中...
js
组件示例:./components/ArticleListts
组件示例:./components/Layout
自己实现了 useI18n()
hooks,支持 SSG 项目,支持实时切换语言
配置文件在 /i18n/config.ts
修改 map
,对应到语言文件
import en from './locales/en-US';
import zh from './locales/zh-CN';
export const localeMap = {
'en-US': en,
'zh-CN': zh,
};
之后就可以在需要翻译的页面上调用
const { t, locale, setLocale } = useI18n();
// t: { language:'语言' }
// locale: 'zh-CN'
// setLocale: (local:string) => void
在需要翻译的地方,可以使用 t 下的属性
<h1>{t.name}</h1>
Recoil
状态管理,示例:./pages/examples/recoil-demo.tsx- useRecoilState
axios-hooks
示例:./pages/examples/dynamic-demo.tsx- useAxios
- TODO
- TODO
- atom 状态(查询参数)更新 -> 自动查询数据-> 页面刷新
- recoil reload
const reloadAtom = atom<number>({
key: 'reload',
default: 0,
});
export const useReload = () => {
const setReloadAtom = useSetRecoilState(reloadAtom);
return () => setReloadAtom((id) => id + 1);
};
yarn dev
开发yarn build:static
发布静态网站yarn start
yarn export
Container
Nav
main
Footer
// TODO
监听事件,调用 setState,如果 state 不同,则 React 会安排一次重新渲染。
- useState 在用 typescript 时接受 null 类型
const [data, setData] = useState<null | String>(null);
- useState 修改现有的值,传递一个函数作为参数
const [isOpen, setIsOpen] = useState(false)
<Toggle onClick={() => setIsOpen(isOpen => !isOpen)} />
- useState 复制原有 state,只修改其中一部分
function handleClick(index) {
setState((state) => {
return {
...state,
bookableIndex: index,
};
});
}
- 如果 useState 初始值的计算非常 expensive,那么传递一个函数作为初始值,这样 React 只会在第一次调用组件时计算
const [value, setValue] = useState(() => {
// expensive calculation here
return initialState;
});
如果 state
是 一个 object
, 那么不可以直接修改,而应该先拷贝一个新的 state 再修改,会比较麻烦。
这个时候就可以使用 Immer
的 produce
函数,更好的控制 state 的更新
import produce from 'immer';
interface State {
readonly x: number;
}
// `x` cannot be modified here
const state: State = {
x: 0,
};
const newState = produce(state, (draft) => {
// `x` can be modified here
draft.x++;
});
在 setState
中用 Immer
比如有如下 state
type ArticleType = { title: string };
const [articles, setArticles] = useState<ArticleType[]>([]);
则可以
setArticles((articles) => {
return produce(articles, (draft) => {
draft.push({ title: `随机Title ${new Date().toISOString()}` });
});
});
在用来设置 state
的时候,可简写
setArticles(
produce((draft) => {
draft.push({ title: `随机Title ${new Date().toISOString()}` });
})
);
示例见 ./pages/examples/dynamic-immer.tsx
When you find you always need to update multiple state values together or your state update logic is so spread out that it’s hard to follow, it might be time to define a function to manage state updates for you: a reducer function
useReducer
是 useState
的进化版本, 避免直接修改状态,使用 dispatcher
广播事件到 reducer
函数里统一处理,个人觉得没有必要。
什么是 Side Effects
Component side effects React components generally transform state into UI. When component code performs actions outside this main focus—perhaps fetching data like blog posts or stock prices srom the network, setting up a subscription to an online service, or directly interacting with the DOM to focus form fields or measure element dimensions—we describe those actions as component side effects.
- 每次渲染后调用
useEffect(() => {
console.log('Running side effects after every render');
});
- 仅在组件
mount
时调用
useEffect(() => {
// 第二个参数传空数组
}, []);
- 在依赖变量改变时调用
useEffect(() => {
// perform effect
// that uses dep1 and dep2
}, [dep1, dep2]);
如果用到 parent
组件传进来的 setState
或者 dispatch
函数,记得添加到依赖里
- 清理,return 一个清理函数
useEffect(() => {
function handleResize() {
setSize(getSize()); // 浏览器大小改变,安排一次重新渲染
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
- async callback
//错误代码
useEffect(async () => {
const resp = await fetch('http://localhost:3001/users');
const data = await resp.json();
setUsers(data);
}, []);
由于 useEffect
需要同步返回一个清理函数,async
函数返回的是一个 promise
,所以会报错
正确方式是把异步访问放在同步函数内部
useEffect(() => {
async function getUsers() {
const resp = await fetch(url);
const data = await resp.json();
setUsers(data);
}
getUsers();
}, []);
useEffect 完整指南 You don't know useEffect
- 对
useEffect
的依赖要诚实,但要尽量清除useEffect
的依赖 - 建议把不依赖
props
和state
的函数提到组件外面 - 把仅被
effect
使用的函数放到effect
里面 useEffect(fn, [])
和componentDidMount
不一样,Effect
拿到的总是定义它的那次渲染中的props
和state
。 所以即便在回调函数里,你拿到的还是初始的props
和state
React will compare objects/functions by their references. There are 2 common cases that you should count when working with dependencies of type object/function:
Case 1: Objects/functions are the same, but the references are different (the case in our example). Case 2: Objects have different values, but their references are the same (this case happens when you partially update the object but don't trigger a re-new action).
useEffect
依赖的函数可以考虑用useCallback
包一层,避免频繁改变
多数情况下,side effect
都是在组件渲染之后同步。 某些特殊情况下副作用导致了立即再重绘,相当于多出来一个中间状态渲染,两次连续渲染导致闪烁的情况出现。
这个时候可以尝试把 useEffect
改成 useLayoutEffect
,表示在 DOM 更新后,但浏览器还没有重绘的时候处理。
大多数时候都不需要用到 useLayoutEffect
,应该在出现问题的时候再尝试使用。
- 与
useState
一样可以保存状态,但不
引起重渲染 - 类似原来类的 实例变量
const ref = useRef(42);
ref.current; // 42
- 保存 timer ID
const timerRef = useRef(null);
useEffect(() => {
timerRef.current = setInterval(() => {
dispatch({ type: 'NEXT_BOOKABLE' });
}, 3000);
return () => {
clearInterval(timerRef.current);
};
}, []);
// 。。。
<button
onClick={() => {
clearInterval(timerRef.current);
}}
>
Stop
</button>;
- 保存 DOM 引用
function Foo() {
// 类型越详细越好 HTMLDivElement > HTMLElement > Element
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// 要先判断是否存在
if (!divRef.current) throw Error('divRef is not assigned');
doSomethingWith(divRef.current);
});
// Give the ref to an element so React can manage it for you
return <div ref={divRef}>etc</div>;
}
useCallback
, 传入一个 inline callback
和一个依赖数组, 保证每次渲染都返回同一个 function
function MyComponent({ prop }) {
const callback = () => {
return 'Result';
};
const memoizedCallback = useCallback(callback, [prop]);
// 这里保证传给 child 的 callback 不会变
return <ChildComponent callback={memoizedCallback} />;
}
不要随便用,出现性能问题再考虑
const memoizedResult = useMemo(() => expensiveFn(a, b), [a, b]);
传入一个 "create" function, 和一个依赖数组。 useCallback(fn, deps)
等于 useMemo(() => fn, deps)
.
比如 ReactTable
示例中用来缓存非常大的 column
数据,而不是每次渲染时都重新声明该变量
const columns = useMemo(
() => [
{
Header: 'Name',
columns: [
{
Header: 'First Name',
accessor: 'firstName',
},
{
Header: 'Last Name',
accessor: 'lastName',
},
],
},
// 很长的数组....
],
[] // 没有依赖
);
- You may rely on useMemo() as a performance optimization, not as a semantic guarantee
- Every value referenced inside the function should also appear in the dependencies array
-
只用来存储不常变的数据
-
注意要把
Provider
抽取出来独立的类管理状态,避免用来管理树顶层state
,会重渲染整个树示例见 ./i18n/Context
-
建议使用
Recoil
的atom
代替Context
使用 useImperativeHandle
需要先了解 forwardRef
的概念
上层想得封装的组件内部 Dom节点
的引用,就需要封装的组件使用 forwardRef
把引用传到底层 Dom
的 ref
上
默认的 React 组件 只接收props
参数,为了使 NestedComponent
接受 ref
,需要用 forwardRef
包装起来
// forwardRef 到 input 类型
const NestedComponentWithForwardRef = forwardRef<HTMLInputElement, Props>(
function NestedComponent(props, frowardedRef) {
return <input {...props} ref={frowardedRef} />;
}
);
forwardRef
的 语法:
// 注意类型顺序与参数顺序相反
const Component = React.forwardRef<RefType, PropsType>((props, ref) => {
return someComponent;
});
之后就可以在上层引用到 input
了
const Index = () => {
// nestedInputRef.current 引用到的是子组件的 input 节点
const nestedInputRef = useRef<HTMLInputElement>(null);
return (
<div>
<NestedComponentWithForwardRef ref={nestedInputRef} />
<Button
onClick={(e) => {
nestedInputRef.current?.focus(); // 调用input的方法
}}
>
</div>
);
};
useImperativeHandle 比 forwardRef
更进了一步,不仅让 Parent
得到 Child
的 Dom
引用,
更提供了把组件内部的 API
暴露给 Parent
的方法, 相当于可以实现从 Parent
调用 Child
的方法,使用方式如下:
// 子组件传递给父组件的api 类型
type ChildAPI = {
focusAndBlur: () => void;
};
// 提供 ChildAPI 的子组件
const NestedComponentWithUseImperativeHandle = forwardRef<ChildAPI, Props>(
function NestedComponent(props, forwardedRef) {
// local ref
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(forwardedRef, () => {
// 把整个api对象返给Parent
return {
// 使input得到焦点,一秒后自动失去焦点
focusAndBlur: () => {
inputRef.current?.focus();
setTimeout(() => {
inputRef.current?.blur();
}, 1000);
},
};
//-------------------------------
});
return <Input {...props} ref={inputRef}></Input>;
}
);
Parent
调用端:
const Index = () => {
const nestedHandleRef = useRef<ChildAPI>(null);
return (
<div>
<NestedComponentWithUseImperativeHandle ref={nestedHandleRef} />
<Button
onClick={(e) => {
nestedHandleRef.current?.focusAndBlur();
}}
>
focusAndBlur
</Button>
</div>
);
};
const [{ data, loading, error }, refetch] = useAxios(
'https://jsonplaceholder.typicode.com/posts'
);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
if (data) {
const articles = data.map((article: any) => {
return {
title: article.title,
};
});
return (
<Flex direction="column" justify="flex-start" my="3">
<ArticleList articles={articles} />
<Button
onClick={() => {
refetch();
}}
>
refetch
</Button>
</Flex>
);
}
atom
是 recoil
版本的 state
const countAtom = atom<number>({
// <number> 是 default 的类型, 可省略自行推断
key: 'count-atom',
default: 1,
});
之后就可以跟 setState
一样使用了
const [count, setCount] = useRecoilState(countAtom);
const readOnlyCount = useRecoilValue(countAtom); // 只读版本
const setOnlyCount = useSetRecoilState(countAtom); // 只写版本
selector
可以对 atom
进行修改并返回
export const countSelector = selector<string>({
// <string> 是 get 返回的类型
key: 'count-selector',
get: ({ get }) => {
const count = get(countAtom); // 取countAtom,修改
return count + 'em';
},
});
selector
同 atom
一样可订阅
const iconSize = useRecoilValue(countSelector);
selector
甚至可以 set
export const countSelector = selector<string | number>({
key: 'count-selector',
get: ({ get }) => {
const count = get(countAtom); // 取countAtom,修改
return count + 'em';
},
set: ({ set }, newValue) => {
const value = parseInt((newValue as string).slice(0, -2));
set(countAtom, value);
},
});
atomFamily()
是一个 utils 函数,返回一个 atom 工厂函数,传入该函数唯一的 id
,则返回唯一的 atom
// 类型为 < default 数据类型,id 类型>
const elementPositionStateFamily = atomFamily<number[], number>({
key: 'ElementPosition',
default: [0, 0],
});
// 创建 atom
elementPositionStateFamily(1); // atom1
elementPositionStateFamily(2); // atom2
elementPositionStateFamily(3); // atom3
selectorFamily<返回类型,参数类型>()
const editState = selectorFamily<number, string>({
key: 'editState',
get: (path: string) => () => {
return 1;
},
set:
(path: string) =>
({ set }, newValue) => {
// set( someAtom,newValue);
},
});
// in components
useRecoilValue(editState('mypath/abc'));
例子:
const myNumberState = atom({
key: 'MyNumber',
default: 2,
});
const myMultipliedState = selectorFamily({
key: 'MyMultipliedNumber',
get:
(multiplier) =>
({ get }) => {
return get(myNumberState) * multiplier;
},
// optional set
set:
(multiplier) =>
({ set }, newValue) => {
set(myNumberState, newValue / multiplier);
},
});
function MyComponent() {
// defaults to 2
const number = useRecoilValue(myNumberState);
// defaults to 200
const multipliedNumber = useRecoilValue(myMultipliedState(100));
return <div>...</div>;
}
官网
- https://nextjs.org/
- https://chakra-ui.com/
- https://recoiljs.org/
- https://www.npmjs.com/package/axios-hooks
- https://www.npmjs.com/package/react-table
学习