首先看一个标准的使用reac hooks的案例:
const useUserList = () => {
const [pending, setPending] = useState(false);
const [users, setUsers] = useState([]);
const load = async params => {
setPending(true);
setUsers([]);
const users = await request('/users', params);
setUsers(users);
setPending(false);
};
const deleteUser = useCallback(
user => setUsers(users => without(users, user)),
[]
);
const addUser = useCallback(
user => setUsers(users => users.concat(user)),
[]
);
return [users, {pending, load, addUser, deleteUser}];
};
提供了用户列表,有加载、添加、删除三个功能,如果团队能用到这种粒度,也算前10%水平了吧。
但是,这个hook的实现其实是有问题的,这个hook包含了多方向的功能,让我们拆一拆:
1.加载一个远程数据,并且控制“加载中”状态。
2.往一个数组中增加或删除内容。
3.将一份数据(列表)和这份数据的相关操作(add、delete)合在一起返回。
4.指定加载用户列表这个具体业务场景。
一但这样去拆解,不难发现其实1-3全是通用能力,而不是业务相关的。
所以我得出来一个比较经典的hook的分层拆解的玩法。
状态与操作封装
如同面向对象强调的是状态(properties)与操作(methods)的封装,虽然我们在React里大量追求函数式,但也并不代表我们应该反对面向对象的封装特性。
把一个状态和它强相关的行为放在一起,显而易见地是一种合理的编程模式。
因此,在hook分层的最底层,我建议大家都有一个功能有,叫作“给我一个值和一堆方法,我帮你变成hook”,在我的实现里我叫它useMethods。这个东西超容易实现:
export const useMethods = (initialValue, methods) => {
const [value, setValue] = useState(initialValue);
const boundMethods = useMemo(
() => Object.entries(methods).reduce(
(methods, [name, fn]) => {
const method = (...args) => {
setValue(value => fn(value, ...args));
};
methods[name] = method;
return methods;
},
{}
),
[methods]
);
return [value, boundMethods];
};
什么你说太绕了都快晕了?玩React哪有不绕的道理……
封装常用数据结构
有了与任何类型都无关的基础的方法封装,我们就可以在它的基础上衍生出最常见的数据结构了。正如原生的数组有push、pop、slice等方法,原生的字符串有trim、padStart、repeat等方法,把这些东西包一包也能变成“数组hook”、“字符串hook”这样的基础hook。这里需要注意的是,你不能把useArray的push直接引到数组的push上去,因为我们对状态的更新要求是immutable的,所以push要对应concat,pop要对应slice,总之这是很容易的:
const arrayMethods = {
push(state, item) {
return state.concat(item);
},
pop(state) {
return state.slice(0, -1);
},
slice(state, start, end) {
return state.slice(start, end);
},
empty() {
return [];
},
set(state, newValue) {
return newValue;
},
remove(state, item) {
const index = state.indexOf(item);
if (index < 0) {
return state;
}
return [...state.slice(0, index), ...state.slice(index + 1)];
}
};
const useArray = (initialValue = []) => {
invariant(Array.isArray(initialValue), 'initialValue must be an array');
return useMethods(initialValue, arrayMethods);
};
相应的,数字我们也可以玩一玩:
const numberMethods = {
increment(value) {
return value + 1;
},
decrement(value) {
return value - 1;
},
set(current, newValue) {
return newValue;
}
};
const useNumber = (initialValue = 0) => {
invariant(typeof initialValue === 'number', 'initialValue must be an number');
return useMethods(initialValue, numberMethods);
};
随你高兴吧,有闲情的可以把什么链表、树、队列、栈、堆、冠军树、红黑树全给来一遍,你高兴就好。
通用过程封装
数据结构毕竟还只是最基础的东西,我们不能只有数据结构就去写代码,我们还需要利用数据结构串起来的过程。
比如在最前面的例子里,对“异步调用”这个事情就是一个很经典的过程。
因此,我们可以有这样的一个hook,它的作用是“给我一个异步函数,我帮你调用它并管理异步状态”,我叫它useTaskPending,功能也简单,直接用useNumber去管一管异步状态就好:
const useTaskPending = task => {
const [pendingCount, {increment, decrement}] = useNumber(0);
const taskWithPending = useCallback(
async (...args) => {
increment();
const result = await task(...args);
decrement();
return result;
},
[task, increment, decrement]
);
return [taskWithPending, pendingCount > 0];
};
再给它进一步,我们想要不仅仅能调用过程,还能把结果给同步到状态里:
const useTaskPendingState = (task, storeResult) => {
const [taskWithPending, pending] = useTaskPendingState(task);
const callAndStore = useCallback(
() => {
const result = await taskWithPending();
storeResult(result);
},
[taskWithPending, storeResult]
);
return [callAndStore, pending];
};
拼装成业务
有数据结构,有过程,现在再去拼一个业务就简单了,像这样:
const useUserList = () => {
const [users, {push, remove, set}] = useArray([]);
const [load, pending] = useTaskPendingState(listUsers, set);
return [users, {pending, load, addUser: push, deleteUser: remove}];
};
你可以看到,很直观地是代码少了那么几行,进而每一行代码都有更强的语义化了,比如useArray明确这里就是一个数组,对比useState还要去看参数才知道是数组还是对象干净利落了不少。更重要的是,基于前面的方法、数据结构、过程这3层,你可以更快地搞出“文章列表”、“评论列表”、“用户详情”等等一系列的业务,而不需要重复地去管理pending、管理数组之类的冗余的事情。
兴致有限,就先简单地介绍一下hook最最基础的状态管理部分的实践玩法。顺便这代码能不能跑我不知道,只代表想法不代表实现~其它如context怎么玩、effect怎么玩、ref有多牛逼、memo有多坑、subscription怎么用,甚至怎么快速写一个小型redux等等,就不赘述了。
欢迎关注我的个人公众号【小恶魔君】,全是原创的有趣有料有理有据的内容。
如何去合理使用 React hook? - 转载自知乎