React 基础案例 | 提醒列表和旅游清单列表(一)

前端达人

共 26638字,需浏览 54分钟

 ·

2021-07-14 14:38

一、开篇

大家好,本系列文章小编将和大家一起,从最基础的真实案例实践 React Hook 相关的知识,如果你已经很熟练了 React Hook 相关内容了,本系列文章你可以忽略。

本系列文章由浅入深,将从最简单的案例开始学习,本篇文章将从两个列表的数据渲染开始讲起,一个是从本地文件获取数据、另一个通过接口请求的方式获取数据。

二、案例1:生日列表加载本地数据

如下图所示,本案例从本地数据加载生日列表数据,列表数据包含了用户的头像、姓名、年龄,同时又包含了一个清除数据的按钮。


2.1、创建项目

开始之前,我们通过 create-react-app 命令创建项目 birthday-reminder,删除一些不相关的文件,保留 App.js、index.css、index.js,index.js 的文件内容修改如下:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
,
  document.getElementById('root')
);

// src/index.js

2.2、设计本地文件数据结构

本案例的数据结构比较简单,一个数组对象,包含 id、name(姓名)、age(年龄)、image(图片地址),新建 data.js 数据文件,示例结构如下:

export default [
  {
    id1,
    name'Bertie Yates',
    age29,
    image:
      'https://res.cloudinary.com/diqqf3eq2/image/upload/v1595959131/person-2_ipcjws.jpg',
  },
  {
    id2,
    name'Hester Hogan',
    age32,
    image:
      'https://res.cloudinary.com/diqqf3eq2/image/upload/v1595959131/person-3_rxtqvi.jpg',
  },
  //更多数据...
]

// src/data.js

2.3、新建本地样式文件

基于页面,我们新建 index.css 文件,定义基础的颜色和常用的尺寸变量等,由于代码比较简单,这里就不过多解释啦,直接贴上代码

/*
=============== 
Variables
===============
*/


:root {
  /* dark shades of primary color*/
  --clr-primary-1hsl(16261%89%);
  --clr-primary-2hsl(16260%78%);
  --clr-primary-3hsl(16261%67%);
  --clr-primary-4hsl(16261%57%);
  /* primary/main color */
  --clr-primary-5hsl(16273%46%);
  /* lighter shades of primary color */
  --clr-primary-6#1aa179;
  --clr-primary-7#13795b;
  --clr-primary-8#0d503c;
  --clr-primary-9#06281e;
  /* darkest grey - used for headings */
  --clr-grey-1hsl(21233%89%);
  --clr-grey-2hsl(21031%80%);
  --clr-grey-3hsl(21127%70%);
  --clr-grey-4hsl(20923%60%);
  /* grey used for paragraphs */
  --clr-grey-5hsl(21022%49%);
  --clr-grey-6hsl(20928%39%);
  --clr-grey-7hsl(20934%30%);
  --clr-grey-8hsl(21139%23%);
  --clr-grey-9hsl(20961%16%);
  --clr-white#fff;
  --clr-red-darkhsl(36067%44%);
  --clr-red-lighthsl(36071%66%);
  --clr-green-darkhsl(12567%44%);
  --clr-green-lighthsl(12571%66%);
  --clr-black#222;
  --transition: all 0.3s linear;
  --spacing0.1rem;
  --radius0.25rem;
  --light-shadow0 5px 15px rgba(0000.1);
  --dark-shadow0 5px 15px rgba(0000.4);
  --max-width1170px;
  --fixed-width450px;
  --clr-pink#f28ab2;
}
/*
=============== 
Global Styles
===============
*/


*,
::after,
::before {
  margin0;
  padding0;
  box-sizing: border-box;
}
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
    Ubuntu, Cantarell, 'Open Sans''Helvetica Neue', sans-serif;
  backgroundvar(--clr-pink);
  colorvar(--clr-grey-9);
  line-height1.5;
  font-size0.875rem;
}
ul {
  list-style-type: none;
}
a {
  text-decoration: none;
}
h1,
h2,
h3,
h4 {
  letter-spacingvar(--spacing);
  text-transform: capitalize;
  line-height1.25;
  margin-bottom0.75rem;
}
h1 {
  font-size3rem;
}
h2 {
  font-size2rem;
}
h3 {
  font-size1.25rem;
}
h4 {
  font-size0.875rem;
}
p {
  margin-bottom1.25rem;
  colorvar(--clr-grey-5);
}
@media screen and (min-width: 800px) {
  h1 {
    font-size4rem;
  }
  h2 {
    font-size2.5rem;
  }
  h3 {
    font-size1.75rem;
  }
  h4 {
    font-size1rem;
  }
  body {
    font-size1rem;
  }
  h1,
  h2,
  h3,
  h4 {
    line-height1;
  }
}
/*  global classes */

/* section */
.section {
  width90vw;
  margin0 auto;
  max-widthvar(--max-width);
}

@media screen and (min-width: 992px) {
  .section {
    width95vw;
  }
}

main {
  min-height100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}

.container {
  width90vw;
  margin5rem 0;
  max-widthvar(--fixed-width);
  backgroundvar(--clr-white);
  border-radiusvar(--radius);
  padding1.5rem 2rem;
  box-shadowvar(--dark-shadow);
}
.container h3 {
  font-weight: normal;
  text-transform: none;
  margin-bottom2rem;
}
.person {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap0.75rem;
  margin-bottom1.5rem;
  align-items: center;
}
.person img {
  width75px;
  height75px;
  object-fit: cover;
  border-radius50%;
  box-shadowvar(--light-shadow);
}
.person h4 {
  margin-bottom0.35rem;
}
.person p {
  margin-bottom0;
}
.container button {
  colorvar(--clr-white);
  display: block;
  width100%;
  border-color: transparent;
  backgroundvar(--clr-pink);
  margin2rem auto 0 auto;
  text-transform: capitalize;
  font-size1.2rem;
  padding0.5rem 0;
  letter-spacingvar(--spacing);
  border-radiusvar(--radius);
  outline1px solid rgba(2421381780.8);
  cursor: pointer;
}

/*
/src/index.css
*/

2.4、设计 List 列表组件

接下来我们新建 List.js 组件,用来展示用户的列表信息,组件定义了 people 属性,用于接收 data 的数据,进行渲染列表数据。

我们使用 map 函数渲染列表数据, 同时使用 const {id,name,age,image} =person 来结构化 person的属性,示例代码如下:

import React from 'react';

const List = ({people}) => {
  return (
    <>
        {
            people.map((person)=>{
                const {id,name,age,image} =person;
                return(
                  <article key={id} className='person'>
                      <img src={image} alt={name}/>
                      <div>
                          <h4>{name}</h4>
                          <p>{age} 岁</p>
                      </div>
                  </article>
                );
            })
        }
    </>

  );
};

export default List;

// src/List.js

2.5、加载数据,渲染 LiST 列表数据

最后我们需要在 App.js 文件里,加载 data.js 中的数据,这里我们使用 state hook 函数加载 data.js  文件中的数据,定义 people 数据状态变量接收 data 数据,将其传至 List 列表中的 people 属性中渲染列表数据。最后我们添加清除按钮,使用 setPeople([]) 方法,将列表的数据清空,界面将会重新 re-render,示例代码如下:

import React, { useState } from 'react';
import data from './data';
import List from './List';
function App({
  const [people,setPeople] = useState(data);
  return (
      <main>
        <section className='container'>
          <h3>今日 {people.length} 人过生</h3>
          <List people={people}/>
          <button onClick={()=>setPeople([])}> 清除数据</button>
        </section>
      </main>

  );
}

export default App;

// src/app.js

点击清除按钮后的效果

到这里,本案例就完成了,是一个很基础的示例,但是列表加载数据是一个很常用的场景,值得多多练习。

三、案例2:旅游清单列表请求接口加载数据

首先描述相关需求:

  1. 此案例通过接口请求数据,加载过程中,显示 Loading 状态,加载完成后显示旅游相关的图片、文章的标题、文章的描述、价格;

  2. 文字描述过长时,则会自动省略,点击 Read More 查看完整的介绍;

  3. 如果用户不敢兴趣的话,可以点击 not interested 按钮进行移除;

  4. 如果列表内容都被移除,显示 refresh 刷新按钮,点击后,重新加载旅游清单数据。

具体交互,如下视频所示:

3.1、 创建项目

开始之前,我们通过 create-react-app 命令创建项目 tours,删除一些不相关的文件,保留 App.js、index.css、index.js,index.js 文件内容如下:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
,
  document.getElementById('root')
);

// src/index.js

3.2、设计接口对象数据

基于界面展示的需求,我们的接口数据需要返回一个数组对象,包含:id(主键)、name(标题)、info(信息)、image(图片)、price(价格),本篇示例,提供一个 https://course-api.com/react-tours-project  接口地址,返回如下数据:


3.3、创建 index.css 样式

基于界面样式,我们新建 index.css 样式文件,由于代码比较简单,这里只是贴上相关代码,代码如下所示:

/*
=============== 
Variables
===============
*/


:root {
  /* dark shades of primary color*/
  --clr-primary-1hsl(20586%17%);
  --clr-primary-2hsl(20577%27%);
  --clr-primary-3hsl(20572%37%);
  --clr-primary-4hsl(20563%48%);
  /* primary/main color */
  --clr-primary-5hsl(20578%60%);
  /* lighter shades of primary color */
  --clr-primary-6hsl(20589%70%);
  --clr-primary-7hsl(20590%76%);
  --clr-primary-8hsl(20586%81%);
  --clr-primary-9hsl(20590%88%);
  --clr-primary-10hsl(205100%96%);
  /* darkest grey - used for headings */
  --clr-grey-1hsl(20961%16%);
  --clr-grey-2hsl(21139%23%);
  --clr-grey-3hsl(20934%30%);
  --clr-grey-4hsl(20928%39%);
  /* grey used for paragraphs */
  --clr-grey-5hsl(21022%49%);
  --clr-grey-6hsl(20923%60%);
  --clr-grey-7hsl(21127%70%);
  --clr-grey-8hsl(21031%80%);
  --clr-grey-9hsl(21233%89%);
  --clr-grey-10hsl(21036%96%);
  --clr-white#fff;
  --clr-red-darkhsl(36067%44%);
  --clr-red-lighthsl(36071%66%);
  --clr-green-darkhsl(12567%44%);
  --clr-green-lighthsl(12571%66%);
  --clr-black#222;
  --transition: all 0.3s linear;
  --spacing0.1rem;
  --radius0.25rem;
  --light-shadow0 5px 15px rgba(0000.1);
  --dark-shadow0 5px 15px rgba(0000.2);
  --max-width1170px;
  --fixed-width620px;
}
/*
=============== 
Global Styles
===============
*/


*,
::after,
::before {
  margin0;
  padding0;
  box-sizing: border-box;
}
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
    Ubuntu, Cantarell, 'Open Sans''Helvetica Neue', sans-serif;
  backgroundvar(--clr-grey-10);
  colorvar(--clr-grey-1);
  line-height1.5;
  font-size0.875rem;
}
ul {
  list-style-type: none;
}
a {
  text-decoration: none;
}
h1,
h2,
h3,
h4 {
  letter-spacingvar(--spacing);
  text-transform: capitalize;
  line-height1.25;
  margin-bottom0.75rem;
}
h1 {
  font-size3rem;
}
h2 {
  font-size2rem;
}
h3 {
  font-size1.25rem;
}
h4 {
  font-size0.875rem;
}
p {
  margin-bottom1.25rem;
  colorvar(--clr-grey-5);
}
@media screen and (min-width: 800px) {
  h1 {
    font-size4rem;
  }
  h2 {
    font-size2.5rem;
  }
  h3 {
    font-size1.75rem;
  }
  h4 {
    font-size1rem;
  }
  body {
    font-size1rem;
  }
  h1,
  h2,
  h3,
  h4 {
    line-height1;
  }
}
/*  global classes */

/* section */
.section {
  width90vw;
  margin0 auto;
  max-widthvar(--max-width);
}

@media screen and (min-width: 992px) {
  .section {
    width95vw;
  }
}
.btn {
  backgroundvar(--clr-primary-5);
  display: inline-block;
  padding0.25rem 0.5rem;
  border-radiusvar(--radius);
  text-transform: capitalize;
  colorvar(--clr-white);
  letter-spacingvar(--spacing);
  border-color: transparent;
  cursor: pointer;
  margin-top2rem;
  font-size1.2rem;
}
/*
=============== 
Tours
===============
*/

main {
  width90vw;
  max-widthvar(--fixed-width);
  margin5rem auto;
}
.loading {
  text-align: center;
}
.title {
  text-align: center;
  margin-bottom4rem;
}
.underline {
  width6rem;
  height0.25rem;
  backgroundvar(--clr-primary-5);
  margin-left: auto;
  margin-right: auto;
}

.single-tour {
  backgroundvar(--clr-white);
  border-radiusvar(--radius);
  margin2rem 0;
  box-shadowvar(--light-shadow);
  transitionvar(--transition);
}
.single-tour:hover {
  box-shadowvar(--dark-shadow);
}
.single-tour img {
  width100%;
  height20rem;
  object-fit: cover;
  border-top-right-radiusvar(--radius);
  border-top-left-radiusvar(--radius);
}
.tour-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom1.5rem;
}
.tour-info h4 {
  margin-bottom0;
}
.single-tour button {
  background: transparent;
  border-color: transparent;
  text-transform: capitalize;
  colorvar(--clr-primary-5);
  font-size1rem;
  cursor: pointer;
  padding-left0.25rem;
}
.tour-price {
  colorvar(--clr-primary-5);
  backgroundvar(--clr-primary-10);
  padding0.25rem 0.5rem;
  border-radiusvar(--radius);
}
.single-tour footer {
  padding1.5rem 2rem;
}
.single-tour .delete-btn {
  display: block;
  width200px;
  margin1rem auto 0 auto;
  colorvar(--clr-red-dark);
  letter-spacingvar(--spacing);
  background: transparent;
  border1px solid var(--clr-red-dark);
  padding0.25rem 0.5rem;
  border-radiusvar(--radius);
}

/*
  src/index.css
*/

3.4、创建加载 Loading 组件

在数据请求阶段,我们需要给用户一个数据正在加载中的状态提示,此时我们需要新建一个 Loading 的组件,代码比较简单,新建一个 Loading.js 的文件,示例代码如下:

import React from 'react';

const Loading = () => {
  return (
    <div className="loading">
      <h1>loading...</h1>
    </div>

  );
};

export default Loading;

// src/Loading.js

3.5、创建清单 Tour 卡片组件

由于清单列表中单个卡片的内容比较多,比如清单的图片、标题、描述信息展示以及 readMore 操作按钮查看完整的信息描述、点击 not interested 移除卡片清单,因此我们要单独创建 Tour.js 卡片组件。

这里我们为组件定义 id,image(图片),info(信息),name(标题),price(价格),removeTour(移除事件属性),同时我们定义 readMore 状态(state hook)用来显示或省略详情内容。

import React, { useState } from 'react';

const Tour = ({id,image,info,name,price,removeTour}) => {
  const [readMore,setReadMore] = useState(false);
  return (
      <article className="single-tour">
        <img src={image} alt={name}/>
        <footer>
          <div className="tour-info">
            <h4>{name}</h4>
            <h4 className="tour-price">${price}</h4>
          </div>
          <p>
            {readMore ? info:`${info.substring(0,200)}...`}
            <button onClick={()=>setReadMore(!readMore)}>
                {readMore?'show less':'read more'}
            </button>
          </p>
          <button className='delete-btn' onClick={()=>removeTour(id)}>
              not interested
          </button>
        </footer>
      </article>

  );
};

export default Tour;

// src/Tour.js

3.6、创建 Tours 列表组件

接下来我们创建卡片清单列表,新建 Tours.js 文件,列表组件定义 tours, removeTour 两个属性,tours 属性用于接收父组件传递的数据,removeTour 事件用于删除对应的清单。列表组件通过数组的 map 函数迭代 Tour 卡片组件,渲染父组件传过来的 Data 属性,示例代码如下:

import React from 'react';
import Tour from './Tour';
const Tours = ({tours, removeTour}) => {
  return (
      <section>
        <div className="title">
          <h2>our tours</h2>
          <div className="underline"></div>
        </div>
        <div>
          {tours.map((tour)=>{
            return <Tour key={tour.id} {...tourremoveTour={removeTour} />;
          })}
        </div>
      </section>

  );
};

export default Tours;
// src/Tours.js

3.7、完善 App.js 界面

最后我们完善 App.js 界面,引入 Tours 组件,按照以下思路编写:

  • 定义接口地址 URL 变量,https://course-api.com/react-tours-project
  • 定义加载数据状态和清单数据状态(state hook):loading 和 tours,用来显示加载状态和渲染接口的数据
  • 定义 removeTour 事件,使用 filter 属性删除对应清单。
  • 接下来定义接口请求方法 fetchTours,使用 async,await 方式请求接口,通过 useEffect Hook 调用 fetchTours 方法,最后别忘记 useEffect 的第二个参数 [] 为空数组,只加载一次;
  • 最后使用条件语句,判断数据是否加载中,显示 Loading 组件;接口请求完成时,调用 Tours 组件,显示清单列表;如果清单列表为空,显示 refresh 重新加载数据的按钮,点击时,重新请求接口加载数据;

基于以上的思路,完成后的代码如下所示:

import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'
// ATTENTION!!!!!!!!!!
// I SWITCHED TO PERMANENT DOMAIN
const url = 'https://course-api.com/react-tours-project'
function App({
  const [loading,setLoading]=useState(true);
  const [tours,setTours]=useState([]);

  const removeTour = (id)=>{
    const newTours = tours.filter((tour)=> tour.id !==id);
    setTours(newTours);
  }

  const fetchTours = async ()=>{
    setLoading(true);
    try {
      const response = await fetch(url);
      const tours=await response.json();
      setLoading(false);
      setTours(tours);
    } catch (error) {
      setLoading(false);
      console.log(error);
    }
  }

  useEffect(()=>{
    fetchTours()
  },[])

  if(loading){
    return (
        <main>
          <Loading/>
        </main>

    )
  }

  if(tours.length===0){
    return (
        <main>
          <div className='title'>
            <h2>no tours left</h2>
            <button className='btn' onClick={()=>fetchTours()}>
              refresh
            </button>
          </div>
        </main>

    )
  }

  return (
      <main>
        <Tours tours={tours} removeTour={removeTour}/>
      </main>

  )
}

export default App

到这里,本案例相关所有的代码就完成了,是不是很简单呢。

结束语

今天的两个实例就介绍这里,虽然简单,但是在实际应用场景又很常见,大家还是有必要亲自动手练习的,如果你想获取本案例源码,请关注“前端达人”公众号,回复“b1”,感谢你的阅读。

相关阅读

React Hooks 学习笔记 | State Hook(一)

React Hooks 学习笔记 | useEffect Hook(二)

浏览 23
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报