Create a simple Menu in React

min read

Day 30/100 #100DaysOfCode #React

Hôm này mình chia sẻ với các bạn cách xây dựng một danh mục menu cho website, ở đây mình sử dụng React, mình lên mạng search thấy có nhiều người hỏi làm thế nào dể active một menu khi ta click chuột vào mục hiện tại, thì nó sẽ show ra danh mục con của nó. Chính vì thế mình thử làm thử xem, mà thấy cũng làm được sơ sơ có gì các bạn tìm hiểu thêm nhé,

Okay, ở đây mình cài đặt React với template là typescript nhé

npx create-react-app my-app --template typescript

Sau khi tải về bạn sẽ được một project name "my-app", các file component được xây dựng theo cấu trúc của typescript, mình thì thích typescript hơn, tuy chưa am hiểu hết về nó. Các bạn biết đó, cái gì cũng bắt đầu từ con số 0, từ từ nó mới tăng lên được, cũng như việc tìm hiểu, học hỏi, học ít ít vậy chứ, tới khi bạn gom lại , bạn sẽ nhận ra sự thành quả bấy lâu nay công sức bạn bỏ ra, chém gió xíu đó mà , ok tiếp tục thôi

Bạn mở project lên và cài đặt thư viện này :   "react-router-dom": "^6.2.1", để bạn có thể chuyển qua lại các compoment thông qua các Route được cài đặt theo ý bạn

Mở tập tin index.tsx, ta cần chỉnh lại như code dưới đây, nói vậy chứ không chỉnh gì nhiều, ta chỉ thay đổi import thư viện vừa cài thôi, trong

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

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Để hiểu hơn về cách hoạt động của "react-router-dom" để cài đặt các Router cho nó, bạn có thể xem bài chia sẻ này của mình : URL ROUTER IN REACT

Ok, bạn tạo đường dẫn thư mục như sau : src/components/menu, trong đường dẫn đó, tạo các tập tin sau:

+ Lists.js : Lưu dữ liệu data danh mục của mình

+ Styles.css : các đoạn code css, thiết lập giao diện, nếu bạn nào biết về scss thì viết scss nó hay hơn

+ Menu.tsx : là component, ta cần viết code react trong tập tin này để thiết lập danh mục menu

* src/components/menu/Lists.js : 

export const Lists =[
    {
        "id":1,
        "title":"Home",
        "link":"/",
    },
    {
        "id":2,
        "title":"Category",
        "link":"/category",
        "submenu":[
            {
                "id":1,
                "title":"Iphone",
                "link":"/category/phone",
            },
            {
                "id":2,
                "title":"Nokia",
                "link":"/category/nokia",
            }
        ]
    },
    {
        "id":3,
        "title":"About",
        "link":"/about",
    },
    {
        "id":4,
        "title":"Conatact",
        "link":"/contact",
    }
];

*src/components/menu/Styles.scss

    .menu-parent{
        width:300px;
        margin:50px auto;
        background-color:#EACF0C;
        text-decoration:center;
        border-radius:5px;
        box-sizing:border-box;
        padding:0;
        position: relative; 
    }
    .ul-li{
        display:block;
        list-style: none;
        
    }
    .li-a{
        position: relative;
        color:#fff;
        padding:10px;
        box-sizing:border-box;
        display:block;
    }

    .li-a span{
        position: absolute;
        right: 10px;
        top: 50%;
        transform: translateY(-50%);
        color: red;
    }
   
    .menu-active{
        background-color:#DDDBD4
    }
    
    /* menu-child */
    .menu-child{
        position: relative;
        background-color: #fff;
        display: none;
        transition: display 1s ease-in; 
    }
    .menu-child li{
        height: 0;
        transition: all 1s ease-in; 
    }
    .menu-child li a{
        padding:5px;
        box-sizing: border-box;
        display: block;
        text-decoration: none;
    }

    .menu-active .menu-child{
      display: block;
    }
    

*src/components/menu/Menu.tsx

import React,{useEffect,useRef,useState} from 'react'
import {Link} from "react-router-dom"
import "./Styles.css"
import {Lists} from './Lists';
import {paramsRouter} from '../../interfaces'
type PropsFunction = (params : any) => void;
const Menu =({ data, toggleClickHandler }: { data: paramsRouter, toggleClickHandler: PropsFunction }) =>{
    const listRef = useRef<HTMLDivElement>(null)
    const liRef = useRef<HTMLLIElement>(null);
    const [heightLi,setHeightLi] = useState<number>(0);
    useEffect(() => {
        if (!liRef.current) throw Error("lỗi");
        doSomethingHeight(liRef.current);
    }, [])
    const doSomethingHeight = ({clientHeight} : any) => {
        setHeightLi(clientHeight);
    }
    
    return (
        <div ref={listRef}>
            <ul className="menu-parent">
            {
                Lists.map(item=>{
                    let active="ul-li";
                    let menuchid = false;
                    if(item.id==data.id){
                        active+= " menu-active";
                    }
                    let counterLi = 0;
                    if(item.submenu!==undefined){
                        menuchid=true;
                        counterLi = item.submenu.length;
                    }
                    return(
                        <li className={active} key={item.id} onClick={()=>toggleClickHandler(item)}  ref={liRef}>
                            <Link to={item.link} className="li-a">{item.title} <span>({counterLi})</span></Link>
                            {menuchid && <ul className="menu-child" >
                                {item.submenu?.map(sub=><li key={sub.id} style={{"height":heightLi +"px"}}>
                                    <Link to={sub.link}>{sub.title}</Link>
                                </li>)}
                            </ul>}
                        </li>
                    )
                })
            }
            </ul>
          
        </div>
    )
}
export default Menu;

Trong đoạn code trên mình có import các file như (Styles.css, Lists.js, interface), mình sẽ tạo một interface cho các thuộc tính danh mục menu của mình

Tạo đường dẫn src/interfaces/index.tsx : 

export interface paramsRouter{
    id:number;
    title:string;
    link:string;
}

Tại sao mà mình phải cần tạo interface cho các thuộc tính, à trong typescript nó yêu cầu ta phải gàn buộc các thuộc tính, biến, hàm, ...,để giúp cho đoạn code của ta maintain hơn, nếu có phát hiện lỗi , thì sửa dễ hơn,..., bạn nào thích typescript thì tìm hiểu thêm nhé, mình chỉ biết sơ sơ thôi ::)

Bạn nhìn đoạn code sau đây : {data,toggleClickHandler} là giá trị mà từ component App.tsx nó gửi qua đó nhé như là : <Menu data={active} toggleClickHandler = {toggleClickHandler}/> , data nó bắt buộc phải là kiểu dữ liệu mà interface đã thiết lập các thuộc tính trước đó, còn toggleClickHandler nó là một hàm ta cũng cần khai báo cho nó luôn, type PropsFunction = (params : any) => void;

const Menu =({ data, toggleClickHandler }: { data: paramsRouter, toggleClickHandler: PropsFunction }) 

Tiếp theo bạn nhìn phần này :

const liRef = useRef<HTMLLIElement>(null);
    const [heightLi,setHeightLi] = useState<number>(0);
    useEffect(() => {
        if (!liRef.current) throw Error("lỗi");
        doSomethingHeight(liRef.current);
    }, [])
    const doSomethingHeight = ({clientHeight} : any) => {
        setHeightLi(clientHeight);
    }

Mình có tạo thêm const liRef = useRef<HTMLLIElement>(null) để lấy giá trị cần dùng tại thẻ element li, chúng ta cần lấy clientWidth,clientHeight,...mình tạo sẵn có bạn nào cần dùng thì dùng ấy mà, bạn có thể xem thêm tại đây :REF IN REACT

Tiếp theo đoạn code dưới đây, mình nghĩ cũng đơn giản thôi : 

Lists.map(item=>{
                    let active="ul-li";
                    let menuchid = false;
                    if(item.id==data.id){
                        active+= " menu-active";
                    }
                    let counterLi = 0;
                    if(item.submenu!==undefined){
                        menuchid=true;
                        counterLi = item.submenu.length;
                    }
                    return(
                        <li className={active} key={item.id} onClick={()=>toggleClickHandler(item)}  ref={liRef}>
                            <Link to={item.link} className="li-a">{item.title} <span>({counterLi})</span></Link>
                            {menuchid && <ul className="menu-child" >
                                {item.submenu?.map(sub=><li key={sub.id} style={{"height":heightLi +"px"}}>
                                    <Link to={sub.link}>{sub.title}</Link>
                                </li>)}
                            </ul>}
                        </li>
                    )
                })
            }
            </ul>

Bạn nhìn đoạn code trên

+ Sử dụng Lists để lặp dữ liệu ra, Lists.map()

+ Tạo biến let menuchid = false; để kiểm tra  if(item.id==data.id){} có phải là danh mục đang được active hay không , tại sao biết nó được active, data.id là giá trị được gửi qua từ components App.tsx. sao đó ta chỉ cần so sánh nó thôi

+ if(item.submenu!==undefined) : kiểm tra nó có submenu không, nếu có thì xem nó có bao nhiêu con trong đó

onClick={()=>toggleClickHandler(item)}  : bắt sự kiện function từ component App.tsx, đồng thời gửi dữ liệu , để cập nhật lại menu được active

*src/App.tsx :  ta cấu hình như sau, ta cần import component Menu vào nó nhé

import React, {useState } from 'react';
import './App.css';
import { Routes, Route } from "react-router-dom";
import Menu from './components/menu/Menu';

const App = () => {

  const [active, setActive] = useState({ "id": 1,"title":"Home","link":"/"})

  const toggleClickHandler = (params: any): void => {
    setActive(params)
  }

  return (
    <div>
      <Menu data={active} toggleClickHandler = {toggleClickHandler}/>
    </div>
  );
}

export default App;

Okay, vậy là xong, các bạn có thề chạy thử nhé, npm start nó thôi

x

Ủng hộ tôi bằng cách click vào quảng cáo. Để tôi có kinh phí tiếp tục phát triển Website!(Support me by clicking on the ad. Let me have the money to continue developing the Website!)