如何给 React 增加交互

137 阅读3分钟

在 React 中,随时间变化的数据叫做状态(state)。可以向任意组件添加 state,根据需要更新它。

处理事件

在 JSX 中可以使用事件处理函数。对于原生组件(比如 <button> 等),只能添加浏览器内置事件(比如 onClick)。对于自定义组件,可以使用任意自定义事件处理函数。

export default function App() {
    return (
        <Toolbar 
            onPlayMovie={() => alert('Playing!')}
            onUploadImage={() => alert('Uploading!')}
        />
    );
}

function Toolbar({ onPlayMovie, onUploadImage }) {
    return (
        <div>
            <Button onClick={onPlayMovie}>
                Play Movie
            </Button>
            <Button onClick={onUploadImage}>
                Upload Image
            </Button>
        </div>
    );
}

function Button({ onClick, children }) {
    return (
        <button onClick={onClick}>
            {children}
        </button>
    );
}

State:组件的记忆

可以使用 useState Hook 给组件添加 state。Hooks 是一种特殊函数,可以让组件使用 React 特性(state 就是这些特性之一)。使用 useState 声明一个 state 变量。它接收一个初始值,返回一对值:当前 state 和一个设置 state 的函数。

const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);

渲染和提交

组件要经过 React 渲染,才能在屏幕上显示。理解这个过程的步骤,会帮助你理清代码执行细节,并解释它的行为。

假设你的组件是厨师,可以把配料变大餐。那么,React 就是服务生,他负责收集食客订单和上菜。完整流程分为三个阶段:

  1. 触发渲染(将订单带到厨房)
  2. 渲染组件(从厨房拿食物)
  3. 提交至 DOM(将食物放到餐桌)

image.png

状态就是快照

与普通的 JavaScript 变量不同,React 状态更像一种快照。设置状态不会改变已有的状态变量,而是会触发一次重新渲染。一开始可能会难以理解。

console.log(count); // 0
setCount(count + 1); // 使用 1 请求重新渲染
console.log(count); // 依然是 0!

React 这么做可以帮助你避免某些难以察觉的错误。在下面的聊天程序中,如果你先点击“Send” 按钮,接着把收件人改为 Bob,那么 5 秒钟之后,alert 弹窗会显示哪个姓名呢?

import { useState } from 'react';

export default function Form() {
    const [to, setTo] = useState('Alice');
    const [message, setMessage] = useState('Hello');
    
    function handleSubmit(e) {
        e.preventDefault();
        setTimeout(() => {
            alert(`You said ${message} to ${to}`);
        }, 5000);
    }
    
    return (
        <form onSubmit={handleSubmit}>
            <label>
                To:{' '}
                <select
                    value={to}
                    onChange={e => setTo(e.target.value)}>
                    <option value="Alice">Alice</option>
                    <option value="Bob">Bob</option>
                </select>
            </label>
            <textarea
                placeholder="Message"
                value={message}
                onChange={e => setMessage(e.target.value)}
            />
            <button type="submit">Send</button>
        </form>
    );
}

排队一批状态变更

下面这个组件有 bug:点击 "+3" 只增加一次分值。

import { useState } from 'react';

export default function Counter() {
    const [score, setScore] = useState(0);
    
    function increment() {
        setScore(score + 1);
    }
    
    return (
        <>
            <button onClick={() => increment()}>+1</button>
            <button onClick={() => {
                increment();
                increment();
                increment();
            }}>+3</button>
            <h1>Score: {score}</h1>
        </>
    );
}

状态就是快照解释了原因。设置状态会触发一次重新渲染,但是不会改变当前的状态值。因此执行 setScore(score + 1) 后,score 依然是 0

如果想修复这个问题,可以设置状态时传入 updater 函数。注意,现在不是 setScore(score + 1),而是 setScore(s => s + 1),可以修复 "+3" 按钮。当你要批量处理多个状态变更时,这个很有用。

function increment() {
-    setScore(score + 1);
+    setScore(s => s + 1);
}

更新状态中的对象

状态可以保存任意类型的 JS 值,包括对象。但是你不能直接修改 React 状态中的对象和数组。当你需要更新对象和数组时,需要创建一个新值(或创建一个现存对象的副本),然后使用新值更新状态。

import { useState } from 'react';

export default function Form() {
    const [person, setPerson] = useState({
        name: 'Tony Stark',
        artwork: {
            title: 'IronMan'
        }
    });
    
    function handleNameChange(e) {
        setPerson({
            ...person,
            name: e.target.value,
        });
    }
    
    function handleTitleChange(e) {
        setPerson({
            ...person,
            artwork: {
                ...person.artwork,
                title: e.target.value,
            }
        });
    }
    
    return (
        <>
            <label>
                Name:
                <input
                    value={person.name}
                    onChange={handleNameChange}
                />
            </label>
            
            <label>
                Title:
                <input
                    value={person.artwork.title}
                    onChange={handleTitleChange}
                />
            </label>
        </>
    );
}

如果拷贝对象的代码太多,可以使用第三方库(Immer)减少重复代码。

import { useImmer } from 'use-immer';

export default function Form() {
    const [person, updatePerson] = useImmer({
        name: 'Tony Stark',
        artwork: {
            title: 'IronMan'
        }
    });
    
    function handleNameChange(e) {
        updatePerson(draft => {
            draft.name = e.target.value;
        });
    }
    
    function handleTitleChange(e) {
        updatePerson(draft => {
            draft.artwork.title = e.target.value;
        });
    }
    
    // ...
}

更新状态的数组

数组是另一种可以存储在状态中的可变对象,需要当作只读类型处理。就像对象一样,当更新数组时,需要创建新的副本更新状态。

import { useState } from 'react';

let nextId = 3;
const initialList = [
    { id: 0, title: 'Big Bellies', seen: false },
    { id: 1, title: 'Lunar Landscape', seen: false },
    { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
    const [list, setList] = useState(initialList);
    
    function handleToggle(artworkId, nextSeen) {
        setList(list.map(artwork => {
            if (artwork.id === artworkId) {
                return { ...artwork, seen: nextSeen };
            } else {
                return artwork;
            }
        }));
    }
    
    return (
        <>
            <h1>My list of art to see:</h1>
            <ItemList 
                artworks={list}
                onToggle={handleToggle}
            />
        </>
    );
}

function ItemList({ artworks, onToggle }) {
    return (
        <ul>
            {artworks.map(artwork => (
                <li key={artwork.id}>
                    <label>
                        <input 
                            type="checkbox"
                            checked={artwork.seen}
                            onChange={e => {
                                onToggle(
                                    artwork.id,
                                    e.target.checked
                                );
                            }}
                        />
                        {artwork.title}
                    </label>
                </li>
            ))}
        </ul>
    );
}

参考文献

  1. Adding Interactivity