React 基础案例 | 可折叠的问题列表和按分类展示的美食菜谱(三)

前端达人

共 17490字,需浏览 35分钟

 · 2021-08-10

一、开篇

大家好,本篇文章小编将和大家一起做两个简单的案例——可折叠的问题列表和按分类展示的美食菜谱。这两个案例,我们还是继续练习 useState Hook 的用法。

在前面的两篇文章里我们已经练习过:《React 基础案例 | 提醒列表和旅游清单列表(一)》《React 基础案例  | 支持左右按钮点击查看信息的卡片组件(二)》,为什么还要继练习呢?这就好比学数学,原理公式比较简单,要熟练掌握要多刷题。useState Hook 也类似,看似简单,但是实际应用场景千变万化,要会运用才是关键,因此还是要多实践才能够真正掌握。

好了,废话不多说,这两个案例在我们的项目中比较常见,我们一起动手开始实践吧!

二、可折叠的问题列表

首先,我们先展示下可折叠的问题列表案例,如下视频所示,默认展示问题的标题,点击加号再展示问题的答案,再次点击折叠问题,只显示问题的标题。基于这个效果我们该如何实现呢?

  • 首先通过脚手架创建项目
  • 然后创建基于本地的数据文件用于显示问题列表的数据
  • 创建单条项目的问题组件,用于展示问题,定义折叠事件
  • 创建问题列表组件,加载本地文件数据,渲染单条项目组件

好了基于思路,我们开始动手实践吧


2.1、 创建项目

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

2.2、设计数据结构

接下来我们定义本地文件的数据结构,列表数据结构很简单,我们新建一个 data.js 文件,定义一个数组对象变量 questions,数据对象包含 id,title(问题标题),info(问题详情),数据结构如下:

const questions = [
  {
    id1,
    title'Do I have to allow the use of cookies?',
    info:
      'Unicorn vinyl poutine brooklyn, next level direct trade iceland. Shaman copper mug church-key coloring book, whatever poutine normcore fixie cred kickstarter post-ironic street art.',
  },
  ...//此处省略
  {
    id5,
    title'When do I recieve a password ordered by letter?',
    info:
      'Locavore franzen fashion axe live-edge neutra irony synth af tilde shabby chic man braid chillwave waistcoat copper mug messenger bag. Banjo snackwave blog, microdosing thundercats migas vaporware viral lo-fi seitan ',
  },
]
export default questions

//src/data.js

2.3、单项问题组件

我们继续定义单项问题组件 Question,新建 Question.js 文件,用于显示单个问题项,这里定义组件的 title 标题属性,info 答案详情属性,我们可以通过父组件传值的形式将内容渲染,同时我们定义了 showInfo 数据状态变量,通过更改数据状态的真假状态实现问题答案的折叠。

import React, { useState } from 'react';
import { AiOutlineMinus, AiOutlinePlus } from 'react-icons/ai';
const Question = ({ title, info }) => {
  const [showInfo, setShowInfo] = useState(false);
  return (
    <article className='question'>
      <header>
        <h4>{title}</h4>
        <button className='btn' onClick={() => setShowInfo(!showInfo)}>
          {showInfo ? <AiOutlineMinus /> : <AiOutlinePlus />}
        </button>
      </header>
      {showInfo && <p>{info}</p>}
    </article>

  );
};

export default Question;

//src/Question.js

注:这里我们用到了 react-icons 插件,用于显示“+(加号)”和“-(减号)”图标,安装命令如下 npm install react-icons --save

2.4、列表组件

接下来我们继续在 App.js 完善逻辑,引入本地数据文件 data.js 和 Question 组件,定义 questions 状态变量(state hook),初始数据为  data.js 的数据,然后通过数组的  map 方法迭代,将数据渲染至 Question 组件,示例代码如下,代码比较简单就不解释了。

import React, { useState } from 'react';
import data from './data';
import SingleQuestion from './Question';
function App({
  const [questions, setQuestions] = useState(data);
  return (
    <main>
      <div className='container'>
        <h3>questions and answers about login</h3>
        <section className='info'>
          {questions.map((question) => {
            return (
              <SingleQuestion key={question.id} {...question}></SingleQuestion>
            );
          })}
        </section>
      </div>
    </main>

  );
}

export default App;

//src/App.js

2.5、CSS样式代码

最后,贴上组件相关的CSS的核心代码,代码比较简单,需要源码的可以查看文末的源码获取方式,这里就不解释了。

/*

...省略一些常规变量定义和基础元素定义

*/


/*
=============== 
Questions
===============
*/


main {
  min-height100vh;
  /* using flex because of better browser support */
  display: flex;
  justify-content: center;
  align-items: center;
}

.container {
  width90vw;
  margin5rem auto;
  backgroundvar(--clr-white);
  border-radiusvar(--radius);
  padding2.5rem 2rem;
  max-widthvar(--fixed-width);
  display: grid;
  gap1rem 2rem;
}
.container h3 {
  line-height1.2;
  font-weight500;
}
@media screen and (min-width: 992px) {
  .container {
    display: grid;
    grid-template-columns250px 1fr;
  }
}
.question {
  padding1rem 1.5rem;
  border2px solid var(--clr-grey-special);
  margin-bottom1rem;
  border-radiusvar(--radius);
  box-shadowvar(--light-shadow);
}
.question h4 {
  text-transform: none;
  line-height1.5;
}
.question p {
  colorvar(--clr-grey-3);
  margin-bottom0;
  margin-top0.5rem;
}
.question header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.question header h4 {
  margin-bottom0;
}
.btn {
  background: transparent;
  border-color: transparent;
  width2rem;
  height2rem;
  backgroundvar(--clr-grey-special);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius50%;
  colorvar(--clr-red-special);
  cursor: pointer;
  margin-left1rem;
  align-self: center;
  min-width2rem;
}

/*
src/index.css
*/

到这里可折叠的问题列表我们就完成了,是不是很简单呢,这个示例,会经常在我们的业务场景应用到,虽然简单,还是建议大家亲自动手试试。

三、按分类展示的美食菜谱

接下来我们继续做一个按分类展示的美食菜谱,这个应用场景会经常在我们的业务场景运用到,比如按分类展示文章、图片等数据。案例的展示效果如下视频所示,美食分为 All(所有)、Breakfast、Lunch、Shakes 这四个分类,默认展示所有的美食数据,然后点击对应的分类展示对应的分类的美食信息,美食信息分为美食标题、美食介绍、美食图片、美食的价格。

基于这个案例的展示效果,我们如何开始下手做呢?

  • 首先通过脚手架创建项目
  • 然后设计美食的本地文件的数据结构
  • 接下来新建分类导航组件 Categories,展示分类名称及定义切换菜单的交互事件。
  • 继续新建美食列表组件 Menu,显示对应分类的美食信息
  • 最后在 App.js 页面里, 组装本地文件的数据、分类导航组件、美食列表组件

好了,基于需求的梳理,我们开始动手实践吧!

3.1、 创建项目

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

3.2、设计数据结构

基于案例展示所示,我们每条美食信息包含美食的名称、图片、分类、价格、描述,接下来我们新建data.js 文件,定义 menu 对象数组变量,数据示例如下:

const menu = [
  {
    id1,
    title'buttermilk pancakes',
    category'breakfast',
    price15.99,
    img'./images/item-1.jpeg',
    desc`I'm baby woke mlkshk wolf bitters live-edge blue bottle, hammock freegan copper mug whatever cold-pressed `,
  },
// 此处省略 
.....
];
export default menu;

//src/data.js

3.3 、美食列表组件

接下来我们创建菜单列表组件 Menu.js 文件,用来显示分类下对应的美食数据,代码比较简单,定义了 items 属性,用来接收父组件传递的数据,渲染列表组件,代码比较简单,这里不再解释,示例代码如下:

import React from 'react';

const Menu = ({items}) => {
  return (
      <div className="section-center">
        {items.map((menuItem)=>{
          const {id,title,img,desc,price}=menuItem;
          return(
              <article key={id} className='menu-item'>
                <img src={img} alt={title} className='photo'/>
                <div className="item-info">
                  <header>
                    <h4>{title}</h4>
                    <h4 className="price">${price}</h4>
                  </header>
                  <p className='item-text'>{desc}</p>
                </div>
              </article>
          )
        })}
      </div>

  );
};

export default Menu;

// src/Menu.js

3.4 、美食分类组件

接下来我们继续新建分类组件 Categories.js 文件,这个组件定义了分类属性categories,用来接收父组件传递的数据,同时定义 filterItems 事件属性,将当前选择的分类传递给父组件。基于这个思路,完成后的代码如下所示:

import React from 'react';
import {unstable_renderSubtreeIntoContainer} from "react-dom";

const Categories = ({categories,filterItems}) => {
  return (
      <div className="btn-container">
        {categories.map((category,index)=>{
          return(
              <button
               type="button"
               className="filter-btn"
               key={index}
               onClick={()=>
filterItems(category)}
              >
                {category}
              </button>
          )
        })}
      </div>

  );
};

export default Categories;

// src/Categories.js

3.5、完善App.js 文件

最后我们需要修改 App.js  文件,在这里组装刚才完成的组件和本地数据,最终呈现出视频案例的效果。具体的思路如下:

  • 定义 allCategories 分类数组变量,对本地数据的分类进行去重,显示所有美食的分类
  • 定义 menuItems 美食数据状态变量和 categories 分类数据变量,并分别初始化为所有的美食数据和所有的分类数据。
  • 定义 filterItems 事件函数,接收子组件 Categories 传递过来的分类属性,动态的更改当前分类下的美食数据,重新 re-render 页面数据

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

import React, { useState } from 'react';
import Menu from './Menu';
import Categories from './Categories';
import items from './data';

const allCategories=['all',...new Set(items.map((item)=>item.category))];

function App({
  const [menuItems,setMenuItems]=useState(items);
  const [categories,setCategories]=useState(allCategories);

  const filterItems = (category)=>{
    if(category==='all'){
      setMenuItems(items);
      return;
    }
    const newItems=items.filter((item)=>item.category===category);
    setMenuItems(newItems);
  }

  return (
      <main>
        <section className="menu section">
          <div className="title">
            <h2>our menu</h2>
            <div className="underline"></div>
          </div>

          <Categories categories={categories} filterItems={filterItems}/>
          <Menu items={menuItems} />
        </section>
      </main>

  );
}

export default App;
// src/App.js

3.6、css样式代码

最后,贴上组件相关的CSS的核心代码,代码比较简单,需要源码的可以查看文末的源码获取方式,这里就不解释了。

/*
=============== 
Menu
===============
*/


.menu {
  padding5rem 0;
}
.title {
  text-align: center;
  margin-bottom2rem;
}
.underline {
  width5rem;
  height0.25rem;
  backgroundvar(--clr-gold);
  margin-left: auto;
  margin-right: auto;
}
.btn-container {
  margin-bottom4rem;
  display: flex;
  justify-content: center;
}
.filter-btn {
  background: transparent;
  border-color: transparent;
  font-size1rem;
  text-transform: capitalize;
  margin0 0.5rem;
  letter-spacing1px;
  padding0.375rem 0.75rem;
  colorvar(--clr-gold);
  cursor: pointer;
  transitionvar(--transition);
  border-radiusvar(--radius);
}
.filter-btn:hover {
  backgroundvar(--clr-gold);
  colorvar(--clr-white);
}
.section-center {
  width90vw;
  margin0 auto;
  max-width1170px;
  display: grid;
  gap3rem 2rem;
  justify-items: center;
}
.menu-item {
  display: grid;
  gap1rem 2rem;
  max-width25rem;
}
.photo {
  object-fit: cover;
  height200px;
  width100%;
  border0.25rem solid var(--clr-gold);
  border-radiusvar(--radius);
  display: block;
}
.item-info header {
  display: flex;
  justify-content: space-between;
  border-bottom0.5px dotted var(--clr-grey-5);
}
.item-info h4 {
  margin-bottom0.5rem;
}
.price {
  colorvar(--clr-gold);
}
.item-text {
  margin-bottom0;
  padding-top1rem;
}

@media screen and (min-width: 768px) {
  .menu-item {
    grid-template-columns225px 1fr;
    gap0 1.25rem;
    max-width40rem;
  }
  .photo {
    height175px;
  }
}
@media screen and (min-width: 1200px) {
  .section-center {
    width95vw;
    grid-template-columns1fr 1fr;
  }
  .photo {
    height150px;
  }
}

到这里按分类展示的美食菜谱的案例就介绍到这里,这个案例在实际应用中更常见,建议大家亲自动手练习下。

四、获取源码

好了,本篇文章两个案例就介绍到这里,是不是很简单很基础呢,大家可以点击阅读原文体验本文的两个案例,b3

五、相关阅读

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

ReaReact 基础案例 | 支持左右按钮点击查看信息的卡片组件(二)

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

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

浏览 1
点赞
评论
收藏
分享

手机扫一扫分享

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

手机扫一扫分享

举报