實作過場動畫 - React Transition Group + animate.css

React 過場動畫實作。CSS animation, React.v17

引言

製作過場動畫提昇UX。

React 動畫方案很多。複雜又炫麗的動畫相對的碼量也很驚人。然而我們要的不多只要簡簡單單的過場即可。經研究數天,發現結合 React Transition Groupanimate.css 就能滿足我們的需求。

關鍵源碼紀錄

案例:單一物件過場動畫

使用 CSSTransition 元件搭配 css animation class。 顯示(mount)與移除(unmount)加入過場動畫。

AppForm.tsx
import { Container, } from '@mui/material'
import { Paper } from '@mui/material'
import { H3, H4, AButton } from 'components/highorder'
import { useState, useReducer } from 'react'
// CSS
import { CSSTransition } from "react-transition-group";
import 'animate.css';

export default (props) => {
    const [show, toggleShow] = useReducer(f => !f, true)
    return (
    <Container>
        <H3>CSSTransition sample</H3>
        <AButton mutant='primary' label={`show:${show}`} onClick={toggleShow} />

        <CSSTransition
            in={show}
            timeout={300}
            classNames={{
                enter: "animate__animated",
                enterActive: "animate__fadeInUp",
                exit: "animate__animated",
                exitActive: "animate__fadeOutDown"
            }}
            unmountOnExit
        >
            <Paper sx={{ p: 4 }}>
                <H3>
                    一個動畫元素<br />
                    An animated element
                </H3>
            </Paper>
        </CSSTransition>

        <H4>EOF</H4>
    </Container >
    )
}

案例:多物件過場動畫,如:Todo List

使用TransitionGroupCSSTransition 元件搭配 css animation class。 新增與移除加入過場動畫。

AppForm.tsx
import { Container, Box, Divider, Paper, IconButton } from '@mui/material'
import { List, ListItem, ListItemText, ListItemIcon } from '@mui/material'
import { FormControl, OutlinedInput } from '@mui/material'
import { H3 } from 'components/highorder'
// hooks
import { useState } from 'react'
import { useAppSelector, useAppDispatch } from 'hooks/hooks'
import { addItem, rmvItem, toggleItem } from 'views/demo2/dm2030/todoListSlice'
// icons
import DoneIcon from '@mui/icons-material/CheckCircle';
import UndoIcon from '@mui/icons-material/RadioButtonUnchecked';
import ClearIcon from '@mui/icons-material/Clear'
// CSS
import clsx from 'clsx'
import { TransitionGroup, CSSTransition } from 'react-transition-group'
import 'animate.css'

export default (props) => {
    const [newText, setNewText] = useState('');
    const todoList = useAppSelector(store => store.todoList)
    const dispatch = useAppDispatch()

    return (
    <Container>
        <H3>Todos</H3>

        <Paper sx={{ p: 2 }}>
            <OutlinedInput 
                placeholder="What needs to bo done?"
                fullWidth
                value={newText}
                onChange={e => setNewText(e.target.value)}
                onKeyUp={e => {
                    if (e.key === 'Enter') {
                        dispatch(addItem(newText))
                        setNewText('')
                    }
                }}
            />
            <List>
                <TransitionGroup>
                    {todoList.map((todo) => (
                        <CSSTransition
                            key={todo.id}
                            timeout={400}
                            classNames={{
                                enter: "animate__animated",
                                enterActive: "animate__fadeInUp",
                                exit: "animate__animated",
                                exitActive: "animate__backOutDown"
                            }}
                        >
                            <ListItem sx={{ p: 2, mb: 1 }}
                                secondaryAction={
                                    <IconButton edge="end" onClick={() => dispatch(rmvItem(todo.id))}>
                                        <ClearIcon />
                                    </IconButton>
                                }
                            >
                                <ListItemIcon onClick={() => dispatch(toggleItem(todo.id))} >
                                    {todo.completed ? <DoneIcon color="success" /> : <UndoIcon />}
                                </ListItemIcon>
                                <ListItemText>
                                    {todo.text}
                                </ListItemText>
                            </ListItem>
                        </CSSTransition>
                    ))}
                </TransitionGroup>
            </List>
        </Paper>
    </Container >
    )
}
todoListSlice.ts
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { AppState, AppThunk } from 'store/store'

export interface ToDoItem {
  id: number, // a unique number
  text: string, // the text the user typed in
  completed: boolean, // a boolean flag
  color: string, // An optional color category
}

const initialState: ToDoItem[] = [
  { id: 1, text: 'Learn React', completed: true, color: null },
  { id: 2, text: 'Learn Redux', completed: false, color: 'purple' },
  { id: 3, text: 'Build something fun!', completed: false, color: 'blue' },
]

export const todoListSlice = createSlice({
  name: 'todoList',
  initialState,
  // The `reducers` field lets us define reducers and generate associated actions
  reducers: {
    addItem: (todoList /* state */, action: PayloadAction<string>) => {
      const newId = todoList.reduce((max,cur)=> (max <= cur.id ? cur.id + 1 : max), 1);
      const newItem:ToDoItem ={
        id: newId,
        text: action.payload,
        completed: false,
        color: null
      };      
      todoList.push(newItem)
    },
    rmvItem: (todoList /* state */, action: PayloadAction<number>) => {
      const itemId = action.payload
      const itemIdx = todoList.findIndex(c => c.id === itemId)
      todoList.splice(itemIdx, 1)
    },
    toggleItem: (todoList /* state */, action: PayloadAction<number>) => {
      const itemId = action.payload
      todoList.map(cur => {
        if(cur.id === itemId) cur.completed = !cur.completed;
        return cur
      })
    }
  },
})

export const {
  addItem,
  rmvItem,
  toggleItem,
} = todoListSlice.actions

export default todoListSlice.reducer;

EOF

Last updated