Logo Zephyrnet

Đệ quy trong phản ứng

Ngày:

Đệ quy là một con thú mạnh mẽ. Không gì thỏa mãn tôi hơn là giải quyết một vấn đề với một hàm đệ quy hoạt động liền mạch.

Trong bài viết này, tôi sẽ trình bày một trường hợp sử dụng đơn giản để đưa các kỹ năng đệ quy của bạn vào hoạt động khi xây dựng Thành phần phản ứng Sidenav lồng nhau.

Đang cài đặt

Tôi đang sử dụng phiên bản React 17.0.2

Trước hết, hãy bắt đầu một Ứng dụng React bản soạn sẵn. Đảm bảo rằng bạn đã cài đặt Nodejs trên máy của mình, sau đó nhập:

npx create-react-app sidenav-recursion

trong thiết bị đầu cuối của bạn, trong thư mục bạn đã chọn.
Sau khi hoàn tất, hãy mở trong trình chỉnh sửa mà bạn chọn:

cd sidenav-recursion
code .

Hãy cài đặt Các thành phần được tạo kiểu, mà tôi sẽ sử dụng để tiêm css và làm cho nó trông đáng yêu. Tôi cũng rất thích Phản ứng thành phần cacbon thư viện biểu tượng.

yard add styled-components @carbon/icons-react

và cuối cùng, yarn start để mở trong trình duyệt của bạn.

Được rồi, hãy làm cho ứng dụng này của riêng chúng ta!

Đầu tiên, tôi muốn xóa mọi thứ trong App.css và thay thế bằng:

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

Sau đó, tôi thêm một tệp vào src gọi là styles.js và bắt đầu với mã này:

import styled from "styled-components";
const Body = styled.div`
  width: 100vw;
  height: 100vh;
  display: grid;
  grid-template-columns: 15% 85%;
  grid-template-rows: auto 1fr;
  grid-template-areas: "header header" "sidenav content";
`;
const Header = styled.div`
  background: darkcyan;
  color: white;
  grid-area: header;
  height: 60px;
  display: flex;
  align-items: center;
  padding: 0.5rem;
`;
const SideNav = styled.div`
  grid-area: sidenav;
  background: #eeeeee;
  width: 100%;
  height: 100%;
  padding: 1rem;
`;
const Content = styled.div`
  grid-area: content;
  width: 100%;
  height: 100%;
  padding: 1rem;
`;
export { Body, Content, Header, SideNav };

và sau đó thiết lập App.js như sau:

import "./App.css";
import { Body, Header, Content, SideNav } from "./styles";
function App() {
  return (
    <Body>
      {" "}
      <Header>
        {" "}
        <h3>My Cool App</h3>
      </Header>
      <SideNav>This is where the sidenav goes</SideNav>
      <Content>Put content here</Content>
    </Body>
  );
}
export default App;

Và bạn nên có một cái gì đó như thế này:
hình ảnh

Thật tốt vì đã đạt được điều này! Bây giờ cho các công cụ thú vị.
Đầu tiên, chúng ta cần một danh sách các tùy chọn sidenav, vì vậy hãy ghi một số tùy chọn vào một tệp mới, sidenavOptions.js:

const sidenavOptions = {
  posts: {
    title: "Posts",
    sub: {
      authors: {
        title: "Authors",
        sub: { message: { title: "Message" }, view: { title: "View" } },
      },
      create: { title: "Create" },
      view: { title: "View" },
    },
  },
  users: { title: "Users" },
};
export default sidenavOptions;

Mỗi đối tượng sẽ có một tiêu đề và các đường dẫn lồng nhau tùy chọn. Bạn có thể lồng bao nhiêu tùy thích, nhưng không được nhiều hơn 4 hoặc 5, vì lý do của người dùng!

Sau đó, tôi đã xây dựng kiểu Menu Option của mình và thêm nó vào styles.js

const MenuOption = styled.div`
  width: 100%;
  height: 2rem;
  background: #ddd;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: ${({ level }) => `0 ${0.5 * (level + 1)}rem`};
  cursor: pointer;
  :hover {
    background: #bbb;
  }
  ${({ isTop }) =>
    isTop &&
    css`
      background: #ccc;
      :not(:first-child) {
        margin-top: 0.2rem;
      }
    `}
`;

và nhập nó cho phù hợp. Các hàm theo chuỗi ký tự mà tôi có ở đó cho phép tôi chuyển các đạo cụ qua Thành phần React và sử dụng trực tiếp trong Thành phần được tạo kiểu của tôi. Bạn sẽ thấy cách này hoạt động sau này.

Hàm đệ quy

Sau đó, tôi đã nhập sidenavOptions vào App.js và bắt đầu viết hàm đệ quy trong thành phần App.js:

import { Fragment } from "react";
import "./App.css";
import sidenavOptions from "./sidenavOptions";
import { Body, Content, Header, SideNav, Top } from "./styles";
function App() {
  const [openOptions, setOpenOptions] = useState([]);
  const generateSideNav = (options, level = 0) => {
    return Object.values(options).map((option, index) => {
      const openId = `${level}.${index}`;
      const { sub } = option;
      const isOpen = openOptions.includes(openId);
      const caret = sub && (isOpen ? <CaretDown20 /> : <CaretRight20 />);
      return (
        <Fragment>
          {" "}
          <MenuOption
            isTop={level === 0}
            level={level}
            onClick={() =>
              setOpenOptions((prev) =>
                isOpen ? prev.filter((i) => i !== openId) : [...prev, openId]
              )
            }
          >
            {" "}
            {option.title} {caret}{" "}
          </MenuOption>
          {isOpen && sub && generateSideNav(sub, level + 1)}{" "}
        </Fragment>
      );
    });
  };
  return (
    <Body>
      {" "}
      <Header>
        {" "}
        <h3>My Cool App</h3>
      </Header>
      <SideNav>{generateSideNav(sidenavOptions)}</SideNav>
      <Content>Put content here</Content>
    </Body>
  );
}
export default App;

Hãy từ từ tiêu hóa những gì đang xảy ra ở đây.
đầu tiên, tôi tạo một trạng thái cho phép tôi kiểm soát những tùy chọn tôi đã nhấp và đang “mở”. Đây là nếu tôi đã đi sâu vào một tùy chọn menu ở cấp độ sâu hơn. Tôi muốn các cấp cao hơn tiếp tục mở khi tôi đi sâu hơn vào.

Tiếp theo, tôi ánh xạ qua từng giá trị trong đối tượng ban đầu và gán một openId duy nhất (theo thiết kế) cho tùy chọn.

Tôi phá hủy sub thuộc tính của tùy chọn, nếu nó tồn tại, hãy tạo một biến để theo dõi xem tùy chọn đã cho có đang mở hay không, và cuối cùng là một biến để hiển thị dấu mũ nếu tùy chọn có thể được đào sâu hay không.

Thành phần tôi trả về được bao bọc trong một Fragment vì tôi muốn trả về chính tùy chọn menu và bất kỳ menu con nào đang mở, nếu có, dưới dạng các phần tử anh em.

Sản phẩm isTop prop cung cấp cho thành phần kiểu dáng hơi khác nếu nó là cấp cao nhất trên sidenav.
Sản phẩm level prop đưa ra một vùng đệm cho phần tử sẽ tăng lên một chút khi mức tăng. Khi tùy chọn được nhấp vào, tùy chọn menu sẽ mở hoặc đóng, tùy thuộc vào trạng thái hiện tại của nó và nếu nó có bất kỳ menu con nào.

Cuối cùng là bước đệ quy! Đầu tiên, tôi kiểm tra xem tùy chọn đã cho đã được nhấp vào mở chưa và nó có menu con, sau đó tôi chỉ gọi lại hàm, bây giờ với sub là tùy chọn chính và cấp độ 1 cao hơn. Javascript thực hiện phần còn lại!

hình ảnh

Bạn nên có cái này, hy vọng, vào thời điểm này.

Hãy thêm định tuyến!

Tôi đoán thành phần sidenav tương đối vô dụng trừ khi mỗi tùy chọn thực sự trỏ đến một cái gì đó, vì vậy hãy thiết lập nó. Chúng tôi cũng sẽ sử dụng một hàm đệ quy để kiểm tra xem tùy chọn cụ thể này và cây cha của nó có phải là liên kết hoạt động hay không.
Đầu tiên, hãy thêm Bộ định tuyến phản ứng gói chúng tôi cần:

yarn add react-router-dom

Để truy cập tất cả các chức năng định tuyến, chúng tôi cần cập nhật index.js tập tin để gói mọi thứ trong một BrowserRouter thành phần:

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import App from "./App";
import "./index.css";
import reportWebVitals from "./reportWebVitals"; ReactDOM.render( <React.StrictMode> <Router> <App /> </Router>
 </React.StrictMode>,
 document.getElementById("root")
);


reportWebVitals();

Bây giờ chúng ta cần cập nhật sideNavOptions của mình để bao gồm các liên kết. Tôi cũng thích đặt tất cả các tuyến đường trong dự án của mình trong một cấu hình duy nhất, vì vậy tôi không bao giờ viết mã một tuyến đường. Đây là giao diện sidenavOptions.js được cập nhật của tôi:

const routes = {
  createPost: "/posts/create",
  viewPosts: "/posts/view",
  messageAuthor: "/posts/authors/message",
  viewAuthor: "/posts/authors/view",
  users: "/users",
};
const sidenavOptions = {
  posts: {
    title: "Posts",
    sub: {
      authors: {
        title: "Authors",
        sub: {
          message: { title: "Message", link: routes.messageAuthor },
          view: { title: "View", link: routes.viewAuthor },
        },
      },
      create: { title: "Create", link: routes.createPost },
      view: { title: "View", link: routes.viewPosts },
    },
  },
  users: { title: "Users", link: routes.users },
};
export { sidenavOptions, routes };

Lưu ý rằng tôi không có bản xuất mặc định nữa. Tôi sẽ phải sửa đổi câu lệnh nhập trong App.js để khắc phục sự cố.

import {sidenavOptions, routes} from "./sidenavOptions";

Trong tôi styles.js, Tôi đã thêm một màu xác định vào thành phần MenuOption của mình:

color: #333;

và cập nhật hàm đệ quy của tôi để bao bọc MenuOption trong một thành phần Liên kết, cũng như thêm Định tuyến cơ bản vào nội dung. App.js đầy đủ của tôi:

import { CaretDown20, CaretRight20 } from "@carbon/icons-react";
import { Fragment, useState } from "react";
import { Link, Route, Switch } from "react-router-dom";
import "./App.css";
import { routes, sidenavOptions } from "./sidenavOptions";
import { Body, Content, Header, MenuOption, SideNav } from "./styles";
function App() {
  const [openOptions, setOpenOptions] = useState([]);
  const generateSideNav = (options, level = 0) => {
    return Object.values(options).map((option, index) => {
      const openId = `${level}.${index}`;
      const { sub, link } = option;
      const isOpen = openOptions.includes(openId);
      const caret = sub && (isOpen ? <CaretDown20 /> : <CaretRight20 />);
      const LinkComponent = link ? Link : Fragment;
      return (
        <Fragment>
          {" "}
          <LinkComponent to={link} style={{ textDecoration: "none" }}>
            {" "}
            <MenuOption
              isTop={level === 0}
              level={level}
              onClick={() =>
                setOpenOptions((prev) =>
                  isOpen ? prev.filter((i) => i !== openId) : [...prev, openId]
                )
              }
            >
              {" "}
              {option.title} {caret}{" "}
            </MenuOption>
          </LinkComponent>
          {isOpen && sub && generateSideNav(sub, level + 1)}{" "}
        </Fragment>
      );
    });
  };
  return (
    <Body>
      {" "}
      <Header>
        {" "}
        <h3>My Cool App</h3>
      </Header>
      <SideNav>{generateSideNav(sidenavOptions)}</SideNav>
      <Content>
        {" "}
        <Switch>
          {" "}
          <Route
            path={routes.messageAuthor}
            render={() => <div>Message Author!</div>}
          />{" "}
          <Route
            path={routes.viewAuthor}
            render={() => <div>View Author!</div>}
          />{" "}
          <Route
            path={routes.viewPosts}
            render={() => <div>View Posts!</div>}
          />{" "}
          <Route
            path={routes.createPost}
            render={() => <div>Create Post!</div>}
          />{" "}
          <Route path={routes.users} render={() => <div>View Users!</div>} />{" "}
          <Route render={() => <div>Home Page!</div>} />{" "}
        </Switch>
      </Content>
    </Body>
  );
}
export default App;

Vì vậy, bây giờ, tất cả các định tuyến sẽ được thiết lập và hoạt động.

hình ảnh

Phần cuối cùng của câu đố là xác định xem liên kết có hoạt động hay không và thêm một số kiểu dáng. Bí quyết ở đây không chỉ là xác định Tùy chọn Menu của chính liên kết mà còn đảm bảo kiểu dáng của toàn bộ cây bị ảnh hưởng để nếu người dùng làm mới trang và tất cả các menu bị thu gọn, người dùng vẫn biết cây nào đang giữ. liên kết đang hoạt động, được lồng vào nhau.

Đầu tiên, tôi sẽ cập nhật thành phần MenuOption của mình trong styles.js để cho phép hỗ trợ isActive:

const MenuOption = styled.div`
  color: #333;
  width: 100%;
  height: 2rem;
  background: #ddd;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: ${({ level }) => `0 ${0.5 * (level + 1)}rem`};
  cursor: pointer;
  :hover {
    background: #bbb;
  }
  ${({ isTop }) =>
    isTop &&
    css`
      background: #ccc;
      :not(:first-child) {
        margin-top: 0.2rem;
      }
    `} ${({ isActive }) =>
    isActive &&
    css`
      border-left: 5px solid #333;
    `}
`;

Và App.js cuối cùng của tôi:

import { CaretDown20, CaretRight20 } from "@carbon/icons-react";
import { Fragment, useCallback, useState } from "react";
import { Link, Route, Switch, useLocation } from "react-router-dom";
import "./App.css";
import { routes, sidenavOptions } from "./sidenavOptions";
import { Body, Content, Header, MenuOption, SideNav } from "./styles";
function App() {
  const [openOptions, setOpenOptions] = useState([]);
  const { pathname } = useLocation();
  const determineActive = useCallback(
    (option) => {
      const { sub, link } = option;
      if (sub) {
        return Object.values(sub).some((o) => determineActive(o));
      }
      return link === pathname;
    },
    [pathname]
  );
  const generateSideNav = (options, level = 0) => {
    return Object.values(options).map((option, index) => {
      const openId = `${level}.${index}`;
      const { sub, link } = option;
      const isOpen = openOptions.includes(openId);
      const caret = sub && (isOpen ? <CaretDown20 /> : <CaretRight20 />);
      const LinkComponent = link ? Link : Fragment;
      return (
        <Fragment>
          {" "}
          <LinkComponent to={link} style={{ textDecoration: "none" }}>
            {" "}
            <MenuOption
              isActive={determineActive(option)}
              isTop={level === 0}
              level={level}
              onClick={() =>
                setOpenOptions((prev) =>
                  isOpen ? prev.filter((i) => i !== openId) : [...prev, openId]
                )
              }
            >
              {" "}
              {option.title} {caret}{" "}
            </MenuOption>
          </LinkComponent>
          {isOpen && sub && generateSideNav(sub, level + 1)}{" "}
        </Fragment>
      );
    });
  };
  return (
    <Body>
      {" "}
      <Header>
        {" "}
        <h3>My Cool App</h3>
      </Header>
      <SideNav>{generateSideNav(sidenavOptions)}</SideNav>
      <Content>
        {" "}
        <Switch>
          {" "}
          <Route
            path={routes.messageAuthor}
            render={() => <div>Message Author!</div>}
          />{" "}
          <Route
            path={routes.viewAuthor}
            render={() => <div>View Author!</div>}
          />{" "}
          <Route
            path={routes.viewPosts}
            render={() => <div>View Posts!</div>}
          />{" "}
          <Route
            path={routes.createPost}
            render={() => <div>Create Post!</div>}
          />{" "}
          <Route path={routes.users} render={() => <div>View Users!</div>} />{" "}
          <Route render={() => <div>Home Page!</div>} />{" "}
        </Switch>
      </Content>
    </Body>
  );
}
export default App;

Tôi đang nhận được hiện tại pathname từ useLocation hook trong React Router. Sau đó tôi tuyên bố một useCallback chức năng chỉ cập nhật khi tên đường dẫn thay đổi. Hàm đệ quy này determineActive đưa vào một tùy chọn và nếu nó có liên kết, hãy kiểm tra xem liên kết có thực sự hoạt động hay không, và nếu không, nó sẽ kiểm tra đệ quy bất kỳ menu con nào để xem có liên kết con nào đang hoạt động hay không.

Hy vọng rằng bây giờ thành phần Sidenav đang hoạt động bình thường!

hình ảnh

Và như bạn có thể thấy, toàn bộ cây vẫn hoạt động, ngay cả khi mọi thứ bị sụp đổ:

hình ảnh

Đây là bạn có nó! Tôi hy vọng bài viết này là sâu sắc và giúp bạn tìm thấy các trường hợp sử dụng tốt cho đệ quy trong React Components!

Đang ký,

~ Sean Hurwitz

tại chỗ_img

Tin tức mới nhất

tại chỗ_img

Trò chuyện trực tiếp với chúng tôi (chat)

Chào bạn! Làm thế nào để tôi giúp bạn?