React 包裝 Model Class

tags: Web React
category: Front-End
description: React 包裝 Model Class 類別
created_at: 2021/07/07 23:00:00

cover image

目的

  • Model 抽離,統一格式
  • 處理表單的時候少寫一點 useState

事前準備

  • 裝好 Node.js

先建立你的 React Project

$ npx create-react-app model-app

會使用一個簡單的表單當作範例

整理目錄

<src>
  <app>
    <components>
      List.js
      MyForm.js
    App.js
  <models>
    ModelBase.js
    User.js
  index.js

先不管 models/* 裡面放了什麼,先用傳統的做法做一次

// file: App.js
import {useState} from "react";
import MyForm from "./components/MyForm";
import List from "./components/List";

function App() {
    const [users, setUsers] = useState([]);

    const pushUser = user => {
        const newUsers = [...users];
        newUsers.push(user);
        setUsers(newUsers);
    }

    return (
        <div>
            <MyForm pushUser={pushUser}/>
            <hr/>
            <List users={users}/>
        </div>
    )
}

export default App;
// file: MyForm.js
import {useState} from "react";

function MyForm(props) {
    const [email, setEmail] = useState('')
    const [name, setName] = useState('')
    const [username, setUsername] = useState('')
    const [password, setPassword] = useState('')

    return (
        <form>
            <div>
                <label>
                    Email:
                    <input type="text" onChange={e => setEmail(e.target.value)} defaultValue={email}/>
                </label>
            </div>
            <div>
                <label>
                    Name:
                    <input type="text" onChange={e => setName(e.target.value)} defaultValue={name}/>
                </label>
            </div>
            <div>
                <label>
                    Username:
                    <input type="text" onChange={e => setUsername(e.target.value)} defaultValue={username}/>
                </label>
            </div>
            <div>
                <label>
                    Password:
                    <input type="password" onChange={e => setPassword(e.target.value)} defaultValue={password}/>
                </label>
            </div>
            <div>
                <button type="button" onClick={() => props.pushUser({email, name, username, password})}>Submit</button>
            </div>
        </form>
    )
}

export default MyForm;
// file: List.js
function List(props) {
    const {users} = props;

    return (
        <div>
            List:
            {users.length > 0 ? (
                users.map((user, i) => (
                    <div key={i}>
                        <div>email: {user.email}</div>
                        <div>name: {user.name}</div>
                        <div>username: {user.username}</div>
                        <br/>
                    </div>
                ))
            ) : <div>users is empty.</div>}
        </div>
    )
}

export default List;

接下來你應該會看到很陽春的頁面,如下圖:

新增也運作正常


然後我們寫一下 Model

基底類別

// file: ModelBase.js
import {useState} from "react";

export default class ModelBase {
    init = (props, key) => () => {
        const [state, setState] = useState(undefined);

        Object.assign(this, {
            [key]: state,
            ['set' + key.charAt(0).toUpperCase() + key.substr(1)]: setState
        });

        if (this[key] === undefined) {
            setState(props[key]);
        }
    }

    constructor(props) {
        for (let key in props) {
            this.init.call(this, props, key)();
        }
    }
}

User類別

// file: User.js
import ModelBase from "./ModelBase";

export default class User extends ModelBase {
    constructor(props = {}) {
        super({
            email: props.email || '',
            name: props.name || '',
            username: props.username || '',
            password: props.password || '',
        });
    }
}

稍微描述一下 ModelBase.js 他做了什麼神奇的事情

Hook 這個東西不能在迴圈中使用,也不能在 Class 中使用,所以我們要想辦法繞過他,就產生了下面這一段語法

init = (props, key) => () => {
    const [state, setState] = useState(undefined);

    Object.assign(this, {
        [key]: state,
        ['set' + key.charAt(0).toUpperCase() + key.substr(1)]: setState
    });

    if (this[key] === undefined) {
        setState(props[key]);
    }
}

使用一個函數回傳一個函數,預設給 state 的值為 undefined 這樣的話第一次執行就會把給他的預設值給他 setState 進去。

['set' + key.charAt(0).toUpperCase() + key.substr(1)] 的用處如下:

name => setName

所以這個時候如果你把 user 建立出來在 console.log 出來看一下,會看到:

他不只有 User 類別中定義的那些欄位,還有 useState 回傳的 setState 函數,所以接下來可以把 MyForm.js 改成下面這樣

import User from "../../models/User";

function MyForm(props) {
    const user = new User();

    return (
        <form>
            <div>
                <label>
                    Email:
                    <input type="text" onChange={e => user.setEmail(e.target.value)} defaultValue={user.email}/>
                </label>
            </div>
            <div>
                <label>
                    Name:
                    <input type="text" onChange={e => user.setName(e.target.value)} defaultValue={user.name}/>
                </label>
            </div>
            <div>
                <label>
                    Username:
                    <input type="text" onChange={e => user.setUsername(e.target.value)} defaultValue={user.username}/>
                </label>
            </div>
            <div>
                <label>
                    Password:
                    <input type="password" onChange={e => user.setPassword(e.target.value)}
                           defaultValue={user.password}/>
                </label>
            </div>
            <div>
                <button type="button" onClick={() => props.pushUser(user)}>Submit</button>
            </div>
        </form>
    )
}

export default MyForm;

你可能會覺得,又沒有少什麼東西,行數還是差不多阿!

但是整體邏輯看起來清晰很多,而且因為這只是新增,假設修改要讀入舊資料呢?

因為不想搞得太複雜(處理路由),就先寫死初始資料跟假設我要改第1筆(index=0),所以把 App.js 改成下面這樣

import {useState} from "react";
import MyForm from "./components/MyForm";
import List from "./components/List";

function App() {
    const [users, setUsers] = useState([
        {email: '[email protected]', name: 'Tom', username: 'admin', password: '1234'}
    ]);

    const pushUser = user => {
        const newUsers = [...users];
        newUsers.push(user);
        setUsers(newUsers);
    }

    return (
        <div>
            <MyForm users={users} id={0} pushUser={pushUser}/>
            <hr/>
            <List users={users}/>
        </div>
    )
}

export default App;

然後這時你的 MyForm.js 上面就要變成這樣子

function MyForm(props){
    const {id} = props;
    const user = props.users[id];

    const [email, setEmail] = useState(user.email);
    const [name, setName] = useState(user.name);
    const [username, setUsername] = useState(user.username);
    const [password, setPassword] = useState(user.password);

    // ...

    // 這時的更新函數可能就要丟類似這樣的東西進去 (id, {email, name, username, password})
}

或是這樣子

function MyForm(props){
    const {id} = props;
    const user = props.users[id];

    const [user, setUser] = useState({
        email: user.email,
        name: user.name,
        username: user.username,
        password: user.password
    });

    // ...
    // 後面的 onChange 要變成
    // onChange={e => setUser({...newUser, name: e.target.value})}
}

如果採用 Class 呢?

function MyForm(props){
    const {id} = props;
    const user = props.users[id];

    const newUser = new User(user);

    // ...

    // 這時你的更新函數只要丟類似這樣的東西進去 (id, user)
}

簡潔很多,而且假設今天要多一個欄位或少一個欄位,用 Class 只要改類別就可以,不用增減傳入參數,也不會漏掉。




最後更新時間: 2021年07月07日.