react

📝 8 篇文章
📅 最新: 2024/12/19

flex-direction为row时主动换行

# flex-direction为row时主动换行(React native主动换行) 我今天在写`react-native`时候,遇到了很不舒服的一件事情。因为RN默认是`flex`布局,所以如果我想要实现`inline`模式就只能设置父元素的`flex-direction`的value为`row`。这样如果我们如果想要换行需要怎么处理呢? 要知道,在RN中是没有`<br>`标签的,所以要想换行需要采取一些非常的手段。 那有没有什么好办法能够做到主动换行呢? 在正式开始之前我先上一个例子: ```jsx const arr = [1, 2, 3, 4, 5, 6]; export default function App() { return ( <SafeAreaView style={styles.container}> <View style={{ flexDirection: 'row' }}> {arr.map((item, index) => ( <View> <Text key={index}>{item}</Text> </View> ))} </View> </SafeAreaView> ); } ``` 很明显,上述的代码会生成一个纵向排列的`123456`,如果我们希望在**每三个断一下**,即将纵向的`123456`改成横向的`123 <br> 456`应该如何做呢? 刚开始的时候我问了ChatGPT,他给出的答案是使用伪类撑开元素: ```html <div class="flex-container"> <div class="flex-item">1</div> <div class="flex-item break-after">2</div> <!-- 想要在这个元素后换行 --> <div class="flex-item">3</div> <div class="flex-item">4</div> </div> ``` ```css .flex-container { display: flex; flex-wrap: wrap; flex-direction: row; } .flex-item { width: 100px; margin: 5px; } .break-after::after { content: ""; flex-basis: 100%; } ``` 经过我的尝试,这种方法很遗憾的不起任何作用,而且在RN中使用CSS class并不是很容易。但是他给出的代码确实给了我一些启发,经过一些摸索,我发现**如果一次渲染两个元素,其中一个是正常元素而另一个负责撑开元素**,那么就可以实现换行,代码如下: ```jsx const arr = [1, 2, 3, 4, 5, 6]; export default function App() { return ( <SafeAreaView style={styles.container}> <View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 200 }}> {arr.map((item, index) => { if (item % 3 !== 0) { return <Text key={index}>{item}</Text>; } else { return ( <> {[ <Text key={index}>{item}</Text>, <View style={{ width: '100%' }}></View>, ]} </> ); } })} </View> </SafeAreaView> ); } ``` 但是很抱歉,这样依旧有缺陷,因为reactNative中`<></>`实际上会影响继承,即Text和View将会被看作一个整体,而不会被解析成两个单独的元素,**并且`return`中的`jsx fragment`必须有一个根节点**,也就说说这种方法与顶上charGPT给出的方法并没有区别,都无法将一个元素完美换行。这只会让我们需要换行的元素变为单独的一行而已。 那么,还有什么方法呢? ## 完美的换行 这时候我们或许需要更新一下思路,如果在渲染过程中无法做到完美的换行,那么我们为什么不能在数据上动一点手脚呢?经过一系列思路的转换,我突然想到我们完全可以**进行一个数组分割+元素嵌套的方式实现换行**,代码如下: ```jsx const arr = [1, 2, 3, 4, 5, 6]; export default function App() { const [arrState, setArrState] = useState([]); useEffect(() => { const result = []; let tempArr = []; arr.forEach((item) => { tempArr.push(item); if (item % 3 === 0) { result.push(tempArr); tempArr = []; } }); setArrState(result); }, []); return ( <SafeAreaView style={styles.container}> {arrState.map((subArr, index) => ( <View key={index} style={{ flexDirection: 'row', flexWrap: 'wrap', width: 200 }}> {subArr.map((item, subIndex) => { return <Text key={subIndex}>{item}</Text>; })} </View> ))} </SafeAreaView> ); } ``` 终于,我们实现了完美的换行! so,如果你也想要在flex布局中尝试主动换行,那么希望这篇文章能够解答你的疑惑。 当然,如果你有更好的方法,非常希望能在下方评论区和我分享你的好主意🤗

如何在 react 表达式中传入 react node[] (节点数组)

# 如何在 react 表达式中传入 react node[] (节点数组) 我们知道 jsx 表达式允许插入`reactNode`以及`ReactNode[]`,当但我们传入一个类似的东西的时候他却会报错: ```jsx <div className='App'> { <p>aaa</p> <p>bbb</p> // ❌ JSX expressions must have one parent element } </div> ``` 但是我们明明可以使用 map 函数传入数组呀! ```jsx <div className="App"> {arr.map((num) => ( <p key={num}>{num}</p> ))} </div> ``` 如果我们希望手动在表达式中传入一个节点数组我们应该怎么做呢?当我遇到这种奇葩问题的时候我也是愣了一会的,经过我对`Array.map()`方法的观察,我突然顿悟,如果想要手动传入一个节点数组应该这么写: ```jsx <div className="App">{[<p>aaa</p>, <p>bbb</p>]}</div> ``` 但是回头想想这个问题奇怪的很,我们明明可以写成: ```jsx <div className="App"> <p>aaa</p> <p>bbb</p> </div> ``` 但是其实有时候我们真的会需要手动的传入一个`node[]`,比如有些组件可能只接受一个节点数组而你又只有有限个节点不希望使用`map()`,或者是调试时候使用,再或者是希望在渲染列表中加入几个与之前的节点结构不同的节点,比如以下这个实例: ```jsx <div className="App"> {arr.map((num) => <p key={num}>{num}</p>) .concat([<p>aaa</p>, <p>bbb</p>])} </div> ``` 以上就是如何使用`reactNode[]`的一点个人心得咯

全面理解useEffect——useEffect用法解析与使用技巧

# 全面理解useEffect `useEffect`可以说是react的hook中最强大的一个,他能够以一种极为优雅的方式模拟我们的生命周期。但是优雅的代价就是这个hook极其的晦涩,不容易被理解与掌握。下面我们就来全方位的探讨一下,`useEffect`到底怎么用 ## useEffect的结构 在上一篇关于react的文章我提到过,函数式组件每次`reRender`都会重新执行一遍,如果我们需要生命周期的效果,那我们就需要`useEffect`函数。我们先来看一下useEffect的结构: ```js useEffect(setup, dependencies?) ``` - 参数 - `setup`:一个实现了你需要的副作用逻辑的函数,这个函数将会在组件首次被绑定以及每次**重新渲染导致`dependencies`更新时**调用。同时他还可以`return`一个清除函数,这个函数将会在函数卸载以及每次**重新渲染导致`dependencies`更新之前**调用 - `dependencies`:在`setup`中引用的所有`reactive values`(state、参数以及在组件内部定义的变量和函数)或更新凭据组成的数组。这个数组必须是有限的并且偏平的,每次更新react将使用`Object.is()`比较每个依赖项是否与之前相同,如果不同则更新。 - return - `undefinded` (无返回值) 中文博客中经常会漏掉`setup`函数的返回值,但是它的返回值是这个函数的半壁江山,我们决不能将其忽略不提,如果忽略掉的话我们就有一半的效果无法实现了。 ❗:`useEffect`也是一个hook,请在组件或者自定义hook的最上层使用,不要在循环或分支结构中定义hook ## 模拟生命周期 react生命周期大概分为三种,`mount`、`update`、`unmount`,当然在类式组件,我们已经可以规定是在mount之前,mount时还是mount之后调用,在函数式组件我们就不分的这么清楚了,因为在函数式组件我们的生命周期其实是独立的,是面向状态的。 下面我们就使用useEffect简单模拟一下类式组件的生命周期: **只在初始化时执行(`DidMounted`)** : ```js // 不依赖任何变量,则传入一个空数组 useEffect(() => { console.log("init"); return () => {}; }, []); ``` **每次更新时执行(`render`)** : ```js // 依赖所有变量,则不传入 useEffect(() => { console.log("rerender"); }); ``` **每次卸载时执行(`beforeUnmount`)** : ```js // 不依赖任何变量,return的函数将会在卸载之前被调用 useEffect(() => { return () => {console.log("unmount");}; }, []); ``` 实际上,我们经常说`useEffect`的作用是模拟声明周期,而这似乎违背了函数式组件的初衷,也违背了`useEffect`的初衷。我们应该多从**状态**的角度去思考,`useEffect`并非是什么生命周期函数,而是**状态更新**函数。 ## 控制更新 如果某个变量在每次组件更新的时候都需要更新,那我们完全没有必要使用`useEffect`,我们使用副作用钩子一定是因为我们需要控制某个变量的更新。 我们可以使用`useEffect`将**组件内部的状态与组件外部系统同步**: ```jsx // 比如说,我们可以在父组件更新props的时候调用useEffect const Child = ({ count }) => { const [childContext, setChildContext] = useState(0); useEffect(() => { setChildContext(count); }, [count]); return <p>this is a child, count = {childContext}</p>; }; ``` 或者我们可以使用`useEffect`将**组件组件外部系统与内部的状态同步**: ```jsx import "./styles.css"; import { useEffect, useState } from "react"; const fetch = async (count) => { // 假设这是一个网络请求 // 当然,这个外部状态也可以是一个外部组件,外部变量,jsAPI等等 await new Promise(() => { setTimeout(() => { console.log("fetch date"); }, count); }); }; export default function App() { const [count, setCount] = useState(0); const onClick = () => { setCount(count + 1); }; // 每次修改count都会将更改同步外部系统 useEffect(() => { fetch(count); }, [count]); return ( <div className="App"> <button onClick={onClick}>Change state in father</button> <p>count is {count}</p> </div> ); } ``` 其实生命周期也不过是对状态的统一更改,react现在只是将生命周期将颗粒度从组件降低到了状态。 ## 一些tips ### 在useEffect中使用state 有时候我们可能会需要在`useEffect`中使用和修改`state`,但是这是极端危险的行为,因为当我们修改`state`时会触发页面的`rerender`,然后因为`useEffect`依赖了`state`,这将会导致`state`被再次更改继而引起死循环…… ```jsx const Child = ({ count }) => { const [childContext, setChildContext] = useState(0); useEffect(() => { setChildConte(childContext); }, [count, childContext]); // !! 千万不要这么做,会引发死循环 return <p>this is a child, count = {childContext}</p>; }; ``` 如果我们在`useEffect`中根据之前的state更新state,我们可以这么写: ```jsx useEffect(() => { setChildContext(c => c + 1); }, [count]); // 不需要依赖childContext,安全 ``` 如果你真的既要在Effect中读取state,又要修改state,那么我给出的建议是,:**使用两个依赖同一个更新凭证`useEffect`** 千万别这么做: ```jsx useEffect(() => { setChildContext(Math.random()) console.log(childContext) }, [count, childContext]) // 死循环!!😭 ``` 正确的做法如下: ```jsx useEffect(() => { setChildContext(Math.random()) }, [count]); useEffect(() => { console.log(childContext) }, [count, childContext]) // 安全😊 ``` ### 消除不必要的对象与函数依赖 如果我们在组件内定义了一个函数或对象,我们在上一篇文章详细讲过,它每次都会被初始化。就像这样: ```jsx const childContext = { count: 1 } useEffect(() => { childContext.count = count; console.log("run effect") // 不要这样,每次Render都会触发effect😔 }, [count, childContext]); ``` 取而代之的是,如果你的对象或者函数只在`useEffect`中使用,则写在`setup`内部 ```jsx const Child = ({count}) => { useEffect(() => { const childContext = { count: 1 } childContext.count = count; console.log("run effect") }, [count]); // 推荐,只有在依赖更新的时候才会触发effect 😊 return <p>this is a child</p>; }; ``` 如果你是希望有一个在整个组件都可以被访问的静态变量,那么可以将对象写在组件外部: ```jsx const childContext = { count: 1 } const Child = ({count}) => { useEffect(() => { childContext.count = count; console.log("run effect") }, [count]); // 推荐,只有在依赖更新的时候才会触发effect 😊 return <p>this is a child</p>; }; ``` ## useLayoutEffect 我们在使用`useEffect`的时候有时会出现一种问题:我们的组件会发生闪烁或突然的变化。 这时因为`useEffect`是在`mount`之后执行的,因此第一次渲染出的值是我们定义的初始值,如果更新的性能比较差或者更新的范围比较大,可能会导致我们的`view`发生闪烁的现象。 这个时候我们就可以使用`useLayoutEffect`,他和`useEffect`的最大区别就是他是在`mount`之前执行的,所以不会导致`view`的闪烁。 但是如果我们没有很明显的ui闪烁问题,我们应该尽可能的使用`useEffect`,因为它的运行效率更好。

应该在哪里定义react函数——关于react开发规范的一些理解

# 应该在哪里定义react函数——关于react开发规范的一些理解 ## 前言 我之前有一个同事,从vue技术栈转入react技术栈,对于函数式编程还有一些不太熟悉。 最近他问了我一个问题,**react函数应该定义在什么地方**?他发现有些函数写在react组件之内,有些却写在react函数之外。他想知道哪种做法是最合适的。这是一个很好的问题,因为这体现了对于代码规范的追求。现在我希望在此妥善的梳理一下这个问题。 ## 看法 先开门见山的说一下我的看法,我的看法就是<mark>应外尽外</mark>,意思就是**能够**放到组件外部的函数都应该放到外部。但是需要注意的是,只有满足一定条件的函数才能够放到组件外部。在具体的讨论这些规则之前,我们应该先来看一下函数写在组件内与组件外部的<mark>区别</mark>。 ## 区别 其实两者的区别十分显著,我们在编写`jsx`文件的时候一般每个文件会暴露一个文件入口,也就是我们的组件函数。当我们在父组件使用我们的子组件的时候,实际上就相当于`new`出了一个新的组件实例。而在组件外的函数或者变量,则与组件形成了闭包。(不太了解闭包的可以理解为组件内的东西是原型模式,而组件外的东西是单例模式) 我们来看一个例子: 首先我们有一个Test组件: ```jsx let a = "123"; const Test = (props) => { return( <> <button onClick={()=>{console.log(a);a = props.value;}}>change</button> </> ) } export default Test; ``` 然后我们在App.js中调用这个组件: ```js function App() { return ( <div className="App"> <Test value={666}></Test> <Test value={777}></Test> </div> ); } ``` 可以看到我们在这里调用了两次Test组件,页面上应当有两个按钮。 这时我们应该想一下,当我们依次点击这两个按钮的时候输出的是 > 123 123 还是 > 123 666 呢? 答案显而易见,是`123 666`,各位可以自己去尝试一下。导致这种现象出现的原因其实不是react的组件,而是我们的`esm`。我们应该都对闭包有一定的概念,其实我们的模块化正是使用了闭包这一概念。当我们像上文那样编写Test组件的时候,其实闭包就已经形成了。 下面我们来梳理一下哪些函数是需要放到组件内的以及这其中的原因。 ## 需要在组件内的函数 ### Hook函数 一个hook函数只能在以下两个位置定义: 1. 自定义hook函数内部 2. React组件内部 但是其实我们的自定义hook最终也是在React组件内部调用,所以这就相当于每一个Hook函数最终都必须在React组件内部环境中执行。 ### 依赖Hook函数的函数 上文说过,hook函数只能在React组件内部调用,那么调用hook函数的函数也理所应当的需要写在组件内部。 ### 依赖State的函数 如果我们的函数需要直接使用State的值,那么我们需要将这个函数放置在组件内部,因为state是每一个组件实例私有的,在外部的函数无法读取到这个值。但是对于可能会复用的函数我们还是更推荐大家使用参数传递的方式传递state的值然后再使用setState的方式设置返回值。 不推荐: ```jsx // :( not recommend // a.jsx const [count,setCount] = useState(1); function a(){ setCount(count+2); } // b.jsx const [count,setCount] = useState(2); function b(){ setCount(count+2); } ``` 推荐 ```jsx // :) recommend //calcCount.js function calcCount(count){ return count+2; } //a.jsx const [count,setCount] = useState(1); setCount(calcCount(count)); // b.jsx const [count,setCount] = useState(2); setCount(calcCount(count)); ``` ### 依赖props的函数 依赖props的函数当然也不能将其写在组件外面。我们很大程度上是通过props去区别某一个组件函数的不同实例的,所以props也是组件实例私有的,需要写在外面。 ### 事件处理函数 虽然事件处理函数从理论上来说完全可以放置到组件外部(只要不依赖上面所说的),但是我们会认为事件处理函数是比较私人的东西,从习惯上来说我们还是更习惯将其放到组件的内部而不是抽出到一个util文件中,而且对于不同组件的事件处理函数,我们能抽象的部分也优先。 而且比起将事件处理函数抽象出来,我们更推荐将事件处理函数中处理逻辑的部分代码抽取出来,类似我们在MVC框架中所作的那样,model与controller的分离是值得推崇的设计思想。 ### 其他依赖组件实例的方法、变量的函数 其实我们可以看到,凡是我们的函数依赖了我们组件实例的私有变量、方法,我们都需要放在组件内部。 ## 为啥应外尽外? 以上我们整理出很多需要放到组件内部的情况,那为啥还推荐放到函数外部呢?直接全部放到函数内部不是会简单很多吗? 尽量将函数放到外面其实是一种函数式与模块化的设计思想,他驱使我们尽可能去复用我们的函数以及使用纯函数,综合来说,将函数与变量放到外部有如下几个好处: - 减少内存占用:减少组件内部的函数与变量可以减少组件实例中的内容,减少对内存的占用 - 重用代码:我们完全可以将一些负责逻辑、可以重用的函数归类并且放置到方法类(utils.js之类)中,减少我们的代码量 - 模块化:我们将逻辑与视图抽离完全可以减少我们组件的耦合度,并且可以降低模块体积,提高我们模块化的“效率”。而且能够使我们尽量去使用纯函数编写更优雅、更容易维护的代码 - 共享信息:我们可以刻意的通过将数据放置到组件外让这个组件的不同实例去共享信息。不过需要注意的是,我们放到组件外的数据肯定不是`React State`,这就意味着它们不是响应式的(一般不会有人这么做吧?) 综上所述,对于能够放置到组件函数外的应该尽量放置到函数外,对于能够复用的代码片,最好是能够抽取成全局的函数,这样才更符合模块化的要求。

为何不推荐在react组件中直接定义变量?探秘useRef的使用以及userRef与useState的区别

# 为何不推荐在react组件中直接定义变量?探秘useRef的使用以及userRef与useState的区别 ## 前言 今天在写代码的时候遇到一个问题,我的一个组件需要维护一个不需要在页面中渲染的变量。那么我想,既然它不需要渲染,我直接用`let`变量新建一个不就可以了吗? 但是当我真正用这个`let`创建的变量时,才意识到大事不好,原来这么使用变量有一个大问题! ## 为什么要有hook 要说明问题是什么,我们需要先弄明白一件事情,**我们使用函数式组件时我们怎么去`render`** ?在我们使用类式组件时,`rerender`可能是简单的调用一次`render`方法,那对于函数式组件呢?函数式组件的`render`写在`return`语句中,很明显我们需要重新执行整个函数。 那么我在前言部分提到的问题就呼之欲出了:组件每次渲染更新都会使得变量变为初始值。也就是说我在组件的一个函数中修改变量并更新组件,想在另一个函数中使用时我们获取到的就只会是初始值了。 可以看到,函数式组件是没有状态的!而react引入`hook`的原因就是补全函数式组件在这方面的缺陷。比如useState,当函数重新渲染时,state能够从状态池中获取到上次render的状态,像useEffect能根据变量来确定是否需要渲染,从而起到生命周期函数的作用。 也就是说,如果我们希望有一个能在每次渲染的时候不被重新初始化的变量,那么我们就需要借助hook了。 那么问题又来了,我们应该用哪一个`hook`呢?这里我推荐一个:`useRef`。 ## useRef 我们使用`useRef()`创建的变量不会每次都被重新渲染 ```js let count = 0 // 每次render都会init let count = useRef(0) // rerender时不会init ``` `useRef`的特性如下: - ref的值通过`current`属性获取 - ref是可以更改的 - 更改ref的值**不会**引起`rerender` - 如果ref的值被用于渲染,那么则不可更改 - 不建议在组件渲染中**读**写ref的`current`,推荐在`useEffect`与`event handler`中使用使用`ref.current` 可以看到,ref就是为我们保存**不用每次初始化**而且**不必渲染**的变量而生的。 ### useRef与useState 那么为什么我们要用ref而不去使用state呢?这两个hook有两个非常大的区别: - **ref不会引起rerender,而state会**。当我们调用setState时,会触发rerender,而在使用ref就不必担心这个,你可以随意的使用 - **ref是同步的,state是异步的**。当我们同步的使用useState更新数据之后,我们会获取不到最新的值,因为他是异步更新的。而ref则不会有这样的问题,你可以在更新之后立刻获取到最新的值。 所以对我现在的需求来说,`useRef`要比`useState`合适的多。 ### 使用useRef操作dom useRef的另一个重要的作用就是操作dom,我们在这边也顺带提一下吧。 我们可以这样使用ref: ```jsx const domRef = useRef(null) function focusInput(){ domRef.current.focus(); } return ( <> <input ref={domRef}/> <button onClick={focusInput}>click to focus<button> </> ) ``` 我们通过jsx中的`VNODE`的ref属性属性传递我们的ref,这样react会在渲染的时候自动将`current`的值设置为node。然后我们就可以访问到这个node了。 当这个node被从页面中移除时,`ref.current`也会被设置为`null` ## 总结 如果你希望在react函数式组件中保存一个不会每次渲染时更新并且无须被渲染的值,那么`useRef`是你的不二之选

React Native 移动光标,都什么年代还在用传统setNativeProps

# React Native 移动光标,都什么年代还在用传统setNativeProps? 对我的故事不感兴趣的直接跳转正文部分。 ## 念叨 今天遇到一个bug,要解决这个bug呢就需要手动移动input的光标。然后我发现之前我在~~抄~~借人家的代码的时候已经借过移动光标的代码了: ```typescript inputRef.current.setNativeProps({selection: {start: 1, end: 1}}); ``` 于是我先去搜索了一下`setNativeProps`的用法,在最新版本的RN文档上明确的给出了这个方法的使用示例,但是却没有API文档。。。😓 也就是说,理论上,这个方法是可以使用的,于是我又搜索了一下如何使用这个方法实现移动光标,结果我发现**上述写法是完全正确的,但是完全没用!** 我还发现这些文章还说有一个`_lastNativeSelection`属性可供读取,但是我只读到了`undefined`。于是乎,我大胆的怀疑,这个方法是不是已经寄了😰 然后我打印了一下`inputRef.current`发现有这么一个属性:`"setSelection": [Function setSelection],`,于是乎我大胆尝试,请看下文…… ## 正文 不要再使用`setNativeProps`方法了,这个方法疑似不再受到支持了。如果你需要操作input的光标,请直接使用`setSelection`。示例如下: ```tsx inputRef.current.setSelection(1, 1); ``` 函数签名(<u>我猜的</u>): `setSelection(selectionStart,selectionEnd)` - `selectionStart` - 被选中的第一个字符的位置索引,从 0 开始。如果这个值比元素的 `value` 长度还大,则会被看作 `value` 最后一个位置的索引。 - `selectionEnd` - : 被选中的最后一个字符的 *下一个* 位置索引。如果这个值比元素的 value 长度还大,则会被看作 value 最后一个位置的索引。

react native 如何正确使用realm

# react native 如何正确使用realm Realm是一个支持云同步的快速的新型的数据库,是SQLite的替代品。Realm的官网实际上对Realm的使用有相当详细的讲解,但是对于我们实际开发中可能出现的一些问题官方文档可能会有没有提到的地方,这篇博客旨在记录笔者在使用realm时踩到的一些坑 ## 安装 安装过程官方说的很明确,但是要注意我们需要安装两个依赖: ```shell # 安装realm库 npm install realm # 安装一些操作realm的hooks等 npm install @realm/react ``` 在使用过程中也需要注意,我们引入依赖的位置。 ## 使用 目前的realm支持两种写法,一种是直接引入的方法: ```tsx import React from 'react'; import {RealmProvider} from '@realm/react'; // Import your models import {Profile} from '../../../models'; export const AppWrapper = () => { return ( <RealmProvider schema={[Profile]}> <RestOfApp /> </RealmProvider> ); }; ``` 在你需要的时候仅需要: ```ts import {useQuery} from '@realm/react'; const profiles = useQuery(Profile); ``` 如果你有多个schema,或者说多个表,直接在`RealmProvider`中的`schema`属性中添加即可,因为这里它接受一个**数组**嘛。 ## 更好的使用 但是我认为上述方法并不是最好的使用方法,我这里还是推荐直接使用`createRealmContext`的方式,这样能够让我们更灵活的使用`realm config`。 ```tsx // 配置realm const config: Realm.Configuration = { schema: [Settings, DarftSchema], schemaVersion: 1, onMigration: (oldRealm, newRealm) => { newRealm.deleteAll(); }, }; // 创建Provider const {RealmProvider, useRealm, useObject, useQuery} = createRealmContext(config); // 包裹App剩余部分 function App(): React.JSX.Element { return ( <RealmProvider> <RealmContext.Provider value={{useRealm, useObject, useQuery}}> <LayoutWarp /> </RealmContext.Provider> </RealmProvider> ); } ``` 但是由于我们使用了这种方式之后,就不能使用直接引用的全局api了,所以我们必须想办法把api传递给后代组件。我的建议是:`useContext` ```ts export const RealmContext = createContext({ useRealm, useObject, useQuery, }); ``` 在需要使用这些api的子组件调用: ```ts const {useRealm, useObject} = useContext(RealmContext); const realm = useRealm(); ``` 这样就不会报错了。

基于命令模式使用 React 实现中文自动前进输入框(OTP)

# 基于命令模式使用 React 实现中文自动前进输入框(OTP) 最近写的项目中有一个需要自动换格子、自动聚焦并且让父组件获取输入值的需求。这个需求让我很头大,因为在 RN 中想要控制 DOM 绝非一件容易的事情。 ## 难点分析 - RN 控制 dom 相对麻烦 - 输入的是中文,需要处理输入法或复制粘贴的问题 - 内容需要和父子组件同步 - 格子数量不定长 - 每个 input 相对复杂 ## 解决方案 React 的单向数据流与组件化的模式一定程度上可以提高我们系统的可维护性,而另一方面这种设计也会导致我们的父子组件通信相对麻烦。在此处如果我们想要使用`ref`来维护一个 input 数组这显然是不现实的,因为我们**不能在 for 循环中定义`react hooks`**。而且我的每个 Input 都相对比较复杂,所以我选择每个 input 封装一个组件,然后在组件中使用`ref`来控制`vDom`。那么我们如何通信呢?自然就是**传参**咯。那么子组件如何优雅的通知父组件自身输入的改变呢?经过思索我想到了一种设计模式:<mark>命令模式</mark>。**通过命令模式+状态函数我们可以优雅的将子组件的变化通知父组件**。下面我们先来尝试实现一下最基本的功能,自动跳转与聚焦。 ### 自动聚焦 首先我们需要先思考,一个输入框可能有几种状态,其实无非两种`输入`和`删除`。但是我们是一组输入框,在其中来回跳,所以我们还需要一个`回退`状态。 ```ts // 定义命令格式 interface BaseCommand { name: string; value?: unknown; } interface CheckInputCommand extends BaseCommand { value?: string | undefined; additionalValue?: string; callarIndex: number; } ``` ```tsx // 父组件中定义命令状态 const [command, setCommand] = useState<CheckInputCommand>(); ``` 这样,确定了状态,我们还需要确定一件事情,我们的子组件要接受哪些参数呢? 首先,设置命令的状态函数一定是必须的,另外我们还需要传递一个下标来让组件知道它的序号以及一个`boolean`来表示是否聚焦,代码如下: ```tsx // 定义子组件 type propsType = { setCommand: Dispatch<SetStateAction<CheckInputCommand | undefined>>; index: number; focus: boolean; }; function InputCheck({ setCommand, index, focus, }: propsType): React.JSX.Element {} ``` 并且我们在父组件中应该如是调用子组件: ```tsx // 父组件 // 聚焦元素的下标,默认第一个元素聚焦 const [foucsElement, setFoucsElement] = useState(0); return ( // 此处省略n个字 <View> {arr.map((item, keyIndex) => { return ( <InputCheck key={`${index}-${keyIndex}`} setCommand={setCommand} index={keyIndex} // 下标与聚焦元素的下标相同则聚焦 focus={foucsElement === keyIndex} /> ); })} </View> ); ``` 通过上述的代码,我们已经能够得知我们如何来让一个元素聚焦了:**如果一个元素的下标与我们希望聚焦的元素下标相同则让他聚焦**。我们知道在 react 中,**如果 state 发生了变化则会引起发生变化的节点`rerender`,并且如果组件的 props 发生了变化会引起`reaction`的,这一系列反应让我们可以通过父节点的`state`来控制子组件。** 那么在子组件我们应该如何处理这种变化呢? 那自然是`useEffect`啊! ```jsx // 子组件 const [char, setChar] = useState(""); const inputRef = useRef<any>(); // 处理聚焦 useEffect(() => { if (focus) { inputRef.current.focus(); inputRef.current.setNativeProps({selection: {start: 1, end: 1}}); } }, [focus, setInputValue]); return ( <TextInput value={char} onChange={e => { TextInputHandler(e.nativeEvent.text); }} ref={inputRef} /> ); ``` 好的,这样我们就可以处理父组件传递的`focus`参数并且自动聚焦了。而且通过上面的代码我们也能发现我们返回的`TextInput`是一个<mark>受控组件</mark>哎~那我们又是如何处理输入的呢? 详情还请看代码: ```jsx /** 发送指令的函数 */ const sendCommand = useCallback( (name: string, commandValue?: string, additionalValue?: string) => { const command: CheckInputCommand = { name: name, value: commandValue, callarIndex: index, additionalValue: additionalValue, }; setCommand(command); }, [index, setCommand] ); /** 真正处理输入的逻辑 */ const setInputValue = useCallback( (str: string) => { setChar(displayChar); // 发送命令 sendCommand("input", commandStr, displayChar); }, [sendCommand] ); /** 处理输入 */ const TextInputHandler = (inputChar: string) => { setInputValue(inputChar); }; ``` 这样当我们输入的时候,子组件就已经能够发送命令通知父组件自己的变化了,这时我们就还需要父组件去监听子组件以及做出相应的反应咯: ```jsx // 父组件 /** 监听command变化 */ useEffect(() => { /** 接收子组件发送的命令 */ if (command && command.name === "input") { if (command.callarIndex === format.tunes.length - 1) { return; } setFoucsElement(command.callarIndex + 1); setChars(command.value || ""); } }, [command, format.tunes, format.tunes.length]); ``` 这样我们就可以自动的前进并聚焦啦! ### 处理中文 但是考虑到我们输入的是中文,所以我们必须要对输入进行一定的处理。首先是中文有输入法的问题,**这样我们就需要判断是否是中文,然后只对中文做出相应**。 ```ts // 中文匹配正则 const reg = /^[\u4E00-\u9FA5]+$/; /** 检查是否是中文 */ const verifyCharIsChinese = (char: string) => { if (reg.test(char)) { return true; } return false; }; ``` ```jsx // 子组件 /** 真正处理输入的逻辑 */ const setInputValue = useCallback({ if (!verifyCharIsChinese(str)) { return; } // 截取中文 let displayChar = str.substring(0, 1); let commandStr = str.substring(1); setChar(displayChar); // 发送命令 sendCommand("input", commandStr, displayChar); }, [sendCommand], ); ``` 另外因为中文会存在一次性输入多个字符的特点,所以我们就需要将输入的字符传递给父组件的问题。像上面的代码,我们已经看到我们已经截取了输入,然后发送到父组件,我们又该如何让父组件将多出的字符传递给剩下的子组件呢?**我想到的方法是:`useContext`!** ```tsx // 父组件 export const StrContext: Context<string> = createContext(""); fn(){ //省略函数定义 const [content, setContent] = useState<Array<string>>([]); // 保存真正的内容 if (command && command.name === "input") { useEffect(() => { if (command.callarIndex === format.tunes.length - 1) { return; } setFoucsElement(command.callarIndex + 1); setChars(command.value || ""); } } return( // ...此处省略n-m个字 <StrContext.Provider value={chars}> <View> {arr.map((item, keyIndex) => { return ( <InputCheck key={`${index}-${keyIndex}`} setCommand={setCommand} index={keyIndex} // 下标与聚焦元素的下标相同则聚焦 focus={foucsElement === keyIndex} /> ); })} </View> </StrContext> ) } ``` 同时我们**需要让得到焦点的子组件去处理我们的上下文**,即输入与重新发送指令: ```jsx const value = useContext(StrContext); /** 自动移动字符 */ useEffect(() => { if (focus) { inputRef.current.focus(); inputRef.current.setNativeProps({ selection: { start: 1, end: 1 } }); // 在上文的处理聚焦的函数中添加判断是否有全局字符串的方法 if (value) { setInputValue(value); } } }, [focus, setInputValue, value]); ``` ### 处理删除与后退 如果光前进却不能后退那不是没过河的<font color="green">卒</font>子嘛。看到如何处理前进那么如何处理后退应该也很明确了,但是需要注意,删除与后退并不一样, - **删除**:输入为空 - **后退**:输入已经为空并且继续删除 我们先来实现删除,删除非常简单,在处理输入的时候判断是否为空即可: ```jsx // 子组件 /** 真正处理输入的逻辑 */ const setInputValue = useCallback( (str: string) => { // 输入为空则发送删除命令 if (!str) { setChar(""); sendCommand("delete"); return; } if (!verifyCharIsChinese(str)) { return; } let displayChar = str.substring(0, 1); let commandStr = str.substring(1); setChar(displayChar); // 发送命令 sendCommand("input", commandStr, displayChar); }, [sendCommand] ); ``` <u>_父组件该如何处理我这里就掠过咯_</u>~ 处理后退要稍微复杂一些,首先我们需要在子组件中新增一个监听,监听我们的键盘输入: ```tsx // 子组件 /** 监听键盘事件,处理退格事件 */ const backSpaceHandler = (keyName: string) => { if (keyName === "Backspace" && !char) { sendCommand("back"); } }; return ( <TextInput style={TuneStyle} value={char} onChange={(e) => { TextInputHandler(e.nativeEvent.text); }} onKeyPress={(e) => { backSpaceHandler(e.nativeEvent.key); }} ref={inputRef} /> ); ``` 然后父组件也需要处理退格事件: ```jsx // 父组件 else if (command && command.name === "back") { if (command.callarIndex === 0) { return; } setFoucsElement(command.callarIndex - 1); } ``` ### 内容同步 哎呀,内容同步这种事情读者朋友们自己个性化发挥就好,毕竟我在命令模式中携带了挺多参数呢,请大家根据各自的需求随意发挥就好。 ## 完整代码 父组件: ```jsx export const StrContext: Context<string> = createContext(""); export default function fn(): React.JSX.Element { const [command, setCommand] = useState<CheckInputCommand>(); const [foucsElement, setFoucsElement] = useState(0); const [chars, setChars] = useState(""); // 多出的文字,自动填充到下一个block const [content, setContent] = useState<Array<string>>([]); // 保存真正的内容 /** 监听command变化 */ useEffect(() => { /** 接收子组件发送的命令 */ if (command && command.name === "input") { // 添加到内容 if (command.callarIndex >= 0) { setContent(e => { e[command.callarIndex] = addContentChar( command.additionalValue || "", command.callarIndex, ); return [...e]; }); } if (command.callarIndex === format.tunes.length - 1) { return; } setFoucsElement(command.callarIndex + 1); setChars(command.value || ""); } else if (command && command.name === "delete") { setContent(e => { e[command.callarIndex] = ""; return [...e]; }); } else if (command && command.name === "back") { if (command.callarIndex === 0) { return; } setFoucsElement(command.callarIndex - 1); } }, [command]); return ( <StrContext.Provider key={index} value={chars}> <View style={fillPoemStyle.inlineContainer} key={index}> {arr.map((item, keyIndex) => { return ( <InputCheck tune={item.tune} rhythm={item.rhythm} key={`${index}-${keyIndex}`} setCommand={setCommand} index={item.index as number} focus={foucsElement === item.index} rhymeWord={rhymeWord} /> ); })} </View> </StrContext.Provider> } ``` 子组件 ```jsx type propsType = { setCommand: Dispatch<SetStateAction<CheckInputCommand | undefined>>; index: number; focus: boolean; }; function InputCheck({ setCommand, index, focus, }: propsType): React.JSX.Element { const [char, setChar] = useState(""); const inputRef = useRef<any>(); const value = useContext(StrContext); const sendCommand = useCallback( (name: string, commandValue?: string, additionalValue?: string) => { const command: CheckInputCommand = { name: name, value: commandValue, callarIndex: index, additionalValue: additionalValue, }; setCommand(command); }, [index, setCommand], ); /** 真正处理输入的逻辑 */ const setInputValue = useCallback( (str: string) => { if (!str) { setChar(""); sendCommand("delete"); return; } if (!verifyCharIsChinese(str)) { return; } let displayChar = str.substring(0, 1); let commandStr = str.substring(1); setChar(displayChar); // 发送命令 sendCommand("input", commandStr, displayChar); }, [sendCommand], ); /** 自动移动字符 */ useEffect(() => { if (focus) { inputRef.current.focus(); inputRef.current.setNativeProps({selection: {start: 1, end: 1}}); if (value) { setInputValue(value); } } }, [focus, setInputValue, value]); /** 处理输入 */ const TextInputHandler = (inputChar: string) => { setInputValue(inputChar); }; /** 监听键盘事件,处理退格事件 */ const backSpaceHandler = (keyName: string) => { if (keyName === "Backspace" && !char) { sendCommand("back"); } }; return ( <TextInput style={TuneStyle} value={char} onChange={e => { TextInputHandler(e.nativeEvent.text); }} onKeyPress={e => { backSpaceHandler(e.nativeEvent.key); }} ref={inputRef} /> ); } export default InputCheck; ``` types ```ts // 定义命令格式 interface BaseCommand { name: string; value?: unknown; } interface CheckInputCommand extends BaseCommand { value?: string | undefined; additionalValue?: string; callarIndex: number; } ```