React Native基础&入门教程:以一个To Do List小例子,看props和state

本文由葡萄城技术团队于博客园原创并首发

转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。

在上篇中,我们介绍了什么是Flexbox布局,以及如何使用Flexbox布局。还没有看过的小伙伴欢迎回到文章列表点击查看之前的文章了解。

那么,当我们有了基本的布局概念之后,就可以做一些有意思的尝试了。不过,它们会有一个美中不足:只是静静地呆在那里,不接受反馈。换句话说,它们从应用开始到结束,只有一种状态。

注意,上面这句话其实包含了RN中(当然同时也是React中)两个非常重要的概念:

  • 第一,“从应用开始到结束”,意味着它在时间上有一段生命周期(Life Cycle)

  • 第二,应用其实可以拥有很多种状态(State),比如,正常时是一种状态,出错时是另一种状态。而且这些状态能够在某些条件下进行转换。

基本概念:

在RN中,界面的变化对应着程序状态的变化。或者说,界面的变化,正是因为应用的状态发生了转换而导致的。应用的状态主要由两个变量决定,props和state,它们可以存在于继承自React.Component的每一个组件中。state由组件自身定义,用来管理组件及其子组件的状态。而props来自于父组件,在本组件中相当于常量,它的改变方式只能来自于父组件。

在RN中,界面的变化对应着程序状态的变化。或者说,界面的变化,正是因为应用的状态发生了转换而导致的。应用的状态主要由两个变量决定,props和state,它们可以存在于继承自React.Component的每一个组件中。state由组件自身定义,用来管理组件及其子组件的状态。而props来自于父组件,在本组件中相当于常量,它的改变方式只能来自于父组件。

state和props都不允许手动地直接设值。像this.state.a = 1或者this.props.b = 2这种代码是会报错的。要改变state,只能是在本组件中调用this.setState方法。而要改变props,只能依赖于它的值在传下来之前,已经在其父组件中被改变。

既然在组件中,state属性无论从字面含义还是程序语义上,都是用来表示状态的,那么为什么还需要一个props属性呢?

我的理解主要有两个原因。

第一,因为有些组件其实是“无状态”的。它们只是接受父组件传给它们的东西,然后老老实实把它们渲染出来。它们自己内部不保存任何状态,它们只是对父组件状态的反应。或者说:“它们不生产状态,它们只是父组件状态的显示器。”父组件的状态通过props传递给子组件。我们经常会构造这种无状态的组件,因为它职责单一,封装性好,可作为更复杂组件的基石。

第二,由于父组件与子组件之间往往需要联动,props就是最直接的提供联动的手段。父组件中构造子组件时,就像函数调用的传参一样,把需要的东西传给子组件的props。

state和props的重要特点是,默认情况下。当它们改变时,RN会自动东西渲染与之相关的界面以保持和state与props同步。为什么说“默认情况下”,是因为我们可以利用生命周期函数手动“截断”这个渲染逻辑,本文暂不涉及。

另外,在RN中,其实也可以使用不属于props和state的变量,来手动控制组件的状态。但是不推荐这么做。因为这会使状态的控制方法变得不统一,不利于后期维护。

开始尝试:

我们已经可以基于state与props的概念做一个小练习了。它是一个ToDo List,也就是待办列表。大概长下面这个样子:

To Do List草图

我们把它分为两个页面。最左边是添加待办事项的界面,记为ToDoListAdd。中间和最右边其实是同一个界面,记为ToDoListMain,它拥有两种不同状态。

我们先来看ToDoListAdd界面。它有上中下三个部分。最上面是一个可点击返回的头部,中间是用于输入文字的TextInput,底部是一个确认添加的Button。

有没有发现它和上一次我们的flexbox小练习界面很像呢?没错,基于上一篇的布局知识,我们可以方便地把页面修改成这样。

再来看ToDoListMain界面,它与ToDoListAdd很像。头部的"添加"用以跳转至ToDoListAdd。“多选”用以让每一个待办项的Checkbox显示出来,并且显示最下面包含全选Checkbox的footer。

要完整地完成这个应用,本文的篇幅是不够的,后续文章会深入到更加细节的地方。但是首先,本文会介绍如何实现以下基本功能:1.利用state控制编辑状态;2.利用state实现界面的跳转;3.父子组件之间的通信。让我们着手编写程序,穿插着介绍着三点内容。

步骤1,使用flex布局完成ToDoListAdd界面。在根目录新建一个文件ToDoListAdd.js,定义ToDoListAdd类。为更加简洁,这里省去必要组件的引入代码,以及样式代码。

exportdefaultclassToDoListAddextendsComponent<Props>{
constructor(props){
super(props);
}
onPress(){}//暂且为空

render(){
return(
<Viewstyle={styles.container}>
<Viewstyle={styles.header}>
<Textstyle={styles.add}onPress={this.props.onBack}>返回</Text>
</View>
<Viewstyle={styles.body}>
<TextInput/>
</View>
<Viewstyle={styles.footer}>
<Buttontitle="确定"onPress={this.onPress}style={styles.btn}/>
</View>
</View>
);
}
}

步骤2,初步创建ToDoListMain组件。当开始构思这个组件的时候,至少有两件事情是需要考虑的:

待办事项的数据源,应该来自那里?显示和隐藏底部的状态存应该在哪里?

第一个问题,为了叙述方便,我们把待办事项的数据源用变量todoList来表示。 todoList可以理解为一些状态,它是需要在ToDoListMain组件中被显示和操作的。有两个todoList的可选位置,要么放在ToDoListMain组件自身,要么放在ToDoListMain更上一层的组件中。于此同时,当ToDoListAdd组件试图添加一个新的待办事项时,ToDoListAdd组件是需要修改todoList这个数据源的。如果todoList在ToDoListMain组件中,ToDoListAdd组件就需要和ToDoListMain组件进行通信。但这其实就绕了一个圈子,因为从草图的逻辑上看,ToDoListAdd是与ToDoListMain同级的一个界面,它们之间要通信,一般的做法是借助于共同的父组件。所以,就这个例子来说,把数据源就直接放在ToDoListAdd和ToDoListMain共同的父组件中是更方便的选择。接下来会看到,这个共同的父组件就是App.js,它将引入ToDoListAdd和ToDoListMain,我们还会App.js中手动设置渲染选择的逻辑。

第二个问题,显示和隐藏底部是只在ToDoListMain组件中使用的状态,它不与外界有联系,所以放在它内部即可。

经过上面的分析,我们创建初步创建ToDoListMain如下:

exportdefaultclassToDoListmainextendsComponent<Props>{
constructor(props){
super(props);
this.state={
isEditing:false
};

this.onEdit=this.onEdit.bind(this);
this.renderItem=this.renderItem.bind(this);
}

renderFooter(toggleCheckAll,isAllChecked){
if(!this.state.isEditing){
returnnull;
}
constcount=this.props.todoList.filter(item=>item.isChecked).length;
return(
<Viewstyle={styles.footer}>
<CheckBoxonValueChange={toggleCheckAll}value={isAllChecked}/>
<Textstyle={styles.menu}>{`已选择:${count}项`}</Text>
</View>
);
}

onEdit(){
this.setState((prevState)=>{
return{
isEditing:!prevState.isEditing
}
});
}

renderItem(item){
return(<ToDoListItem{...item}
toggleItemCheck={this.props.toggleItemCheck}
isEditing={this.state.isEditing}/>);
}

render(){
const{toggleCheckAll,isAllChecked,onAddItem,todoList}=this.props;

return(
<Viewstyle={styles.container}>
<Viewstyle={styles.header}>
<Textstyle={styles.add}onPress={onAddItem}>添加</Text>
<Textstyle={styles.title}>待办列表</Text>
<Textstyle={styles.multi}
onPress={this.onEdit}>{this.state.isEditing?'取消':'多选'}
</Text>
</View>

<FlatListstyle={styles.body}isEditing={this.state.isEditing}
data={todoList}renderItem={this.renderItem}/>

{this.renderFooter(toggleCheckAll,isAllChecked)}
</View>
);
}
}

我们看到该组件只有一个状态,isEditing。它控制了左上角的文字是"取消"还是"多选",也控制了底部是否显示。

我们在控制底部是否显示时,调用了一个自定义的函数,用它的返回值最为内容插入在调用函数的位置。在RN中,如果在渲染的时候返回null,就表示什么也不渲染。所以调用renderFooter时,在isEditing状态为false时,什么都不渲染。

toggleCheckAll用来控制是否全选待办事项。isAllChecked是判断是否全选。onAddItem用作点击"添加"文字的回调。而todoList就是最重要的待办事项的数据源了。它们都来自ToDoListMain的父组件,通过props传下来。

而ToDoListMain组件内部,有一个onEdit函数,用作右上角"取消"和"多选"文字onPress时的回调。在里面我们看到RN中设置state的正确方式是调用this.setState方法。

另外,为了演示方便,这里使用官方提供的Checkbox组件来表示待办事项是否check了。但这个Checkbox组件的其实只有Android平台才有,iOS下没有。而对iOS的处理,打算在后面的文章中专门分享。

还有一点值得注意的地方,是引入了FlatList组件来对todoList数据源进行渲染。FlatList是官方提供的用意显示列表的组件,老版本的ListView已经被标记为弃用了(deprecated)。FlatList组件对列表的渲染做了许多性能优化和功能增强。我们暂时只是使用它来简单显示待办列表。

每一个待办事项使用了自定义的另一个组件ToDoListItem,我们马上来看看它。

步骤3,实现ToDoListItem组件。它没有自己的状态,也只是对父组件内容的展示。

exportdefaultclassToDoListItemextendsComponent<Props>{
constructor(props){
super(props);
}

render(){
const{toggleItemCheck,item}=this.props;
const{isChecked,detail,level}=item;
constbasicLevelStyle=styles.level;
letspecificLevelStyle;
if(level==='info'){
specificLevelStyle=styles.info;
}elseif(level==='warning'){
specificLevelStyle=styles.warning;
}else{
specificLevelStyle=styles.error;
}

return(
<Viewstyle={styles.container}>
{this.props.isEditing&&<CheckBoxonValueChange={(value)=>toggleItemCheck(item,value)}style={styles.checkbox}value={isChecked}/>}
<Textstyle={styles.detail}>{detail}</Text>
<Viewstyle={[basicLevelStyle,specificLevelStyle]}></View>
</View>
);
}
}

特别是,每一项是否被check,这个状态其实来自于todoList数据源,而每一项的Checkbox的value完全受控于父组件传来的值,所以这种用户输入型的组件,其值完全受控于父组件的props的传值的,也常被称为受控组件(Controlled Component)。

另外,todoList的每一项,我们用level来表示待办项的某种等级,用detail表示它的内容,用isChecked来表示它是否完成。

但是做了这么多,我们还啥都没看到呢。所以,接下来的关键一步,就是把ToDoListMain和ToDoListAdd的渲染逻辑一口气写到App.js中去。

步骤4,写最外层的渲染逻辑。在App.js中,引入

importToDoListMainfrom'./ToDoListMain';
importToDoListAddfrom'./ToDoListAdd';

然后定义App组件

exportdefaultclassAppextendsComponent<Props>{
constructor(props){
super(props);
this.state={
current:'main',todoList:[
{
level:'info',detail:'一般的任务',isChecked:false,key:'0'
},{
level:'warning',detail:'较重要的任务',},{
level:'error',detail:'非常重要且紧急的任务',key:'2'
}
]
}
this.onAddItem=this.onAddItem.bind(this);
this.onBack=this.onBack.bind(this);
this.toggleCheckAll=this.toggleCheckAll.bind(this);
this.toggleItemCheck=this.toggleItemCheck.bind(this);
}

onAddItem(){
this.setState((prevState)=>{
return{
current:'add'
}
});
}

onBack(){
this.setState({
current:'main'
});
}

toggleCheckAll(){
constflag=this.isAllChecked();
constnewTodos=this.state.todoList.map(item=>{
return{
...item,isChecked:!flag
};
});
this.setState({
todoList:newTodos
});
}

toggleItemCheck(item,key){
constnewTodos=this.state.todoList.map(todo=>{
if(todo!==item){
returntodo;
}
return{
...todo,isChecked:!item.isChecked
}
});
this.setState({
todoList:newTodos
});
}

isAllChecked(){
if(!this.state.todoList)returnfalse;
if(this.state.todoList.length===0)returnfalse;
returnthis.state.todoList.every(item=>item.isChecked);
}

render(){
if(this.state.current==='main'){
return(<ToDoListMain
isAllChecked={this.isAllChecked()}
toggleCheckAll={this.toggleCheckAll}
toggleItemCheck={this.toggleItemCheck}
onAddItem={this.onAddItem}
todoList={this.state.todoList}/>);
}else{
return(<ToDoListAddonBack={this.onBack}/>);
}
}
}

我们看到App组件有两个状态,一个是current,用以指定当前渲染的是哪个界面(其实我们这里就两个界面)。另一个是todoList数据源。

界面是如何切换的呢?

观察render函数,里面就是界面渲染逻辑,如果this.state.current的值是main,那么就会渲染App就会渲染ToDoListMain,否则,渲染ToDoListAdd。你可以理解成,我们手动实现了一个特别简单的前端路由。这一切都是基于当state变化时,相应的界面自动重新渲染了。

更具体地来说,我们把onAddItem作为props的一个属性传给ToDoListMain,把onBack也作为一个属性传给ToDoListAdd。所以当它们的头部相应文字被点击时,实际上调用的,是定义在App组件中的回调函数。回调函数修改了current状态,而current状态的修改引起了App的render函数重新被调用,它根据当前的current状态而重新渲染了相应的界面。

todoList中每项的key值是给FlatList作为唯一标识用的。

另外,在setState句子中,我们会构造一个新的变量,然后一把setState,而不是去修改原有的state。这也是RN中的常用做法。对于初学者来说,可能语法有点怪异。不过,这样做是有它的理由的。简单的说,因为RN在底层大量使用了比较对象是否变化的逻辑,如果挨个便利对象的每个属性,而且对象很复杂的话,这个比较的逻辑是很慢的。但是,比较两个对象的引用是否相等却很容易,直接一个表达式就可以了。所以,我们在setState时往往会构造一个新的对象。更深的机理就留给读者去探索啦。

好了,让我们运行起程序,看看效果怎么样吧。

本文通过一个ToDo List的例子,介绍了RN中最基本的两个概念state和props。并简单实现了状态提升、组件间的通信等功能。

不过这个例子还没完。这个ToDo List目前只是一个展示的功能,如何对它们进行编辑、添加、删除,后续会进一步分享。

文章中使用到的源码下载: todo-list.zip

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom