เฮ้เพื่อนๆ หากคุณยังไม่ได้อ่านบทความก่อนหน้าของฉันเกี่ยวกับ "การเขียนโค้ดแอป Todo อย่างง่ายด้วย React Server" อย่าลืมลองดูในขณะที่เราสร้างโค้ดส่วนใหญ่รอบๆ ตัวมัน
หากคุณยังใหม่กับ React Server โปรดดูหน้าแรกที่ https://state-less.cloud/
เริ่มต้นด้วยความคิด แอปรายการที่เรียบง่ายซึ่งช่วยให้คุณสร้างรายการได้มากเท่าที่คุณต้องการและเพิ่มรายการที่ตรวจสอบได้
โดยทั่วไปเราจำเป็นต้องขยายแอปรายการสิ่งที่ต้องทำเพื่อให้สามารถสร้างรายการได้ทันที แทนที่จะมีรายการสิ่งที่ต้องทำแบบคงที่เพียงรายการเดียว
สิ่งนี้มีประโยชน์หากคุณต้องการติดตามหลายรายการในรายการที่แตกต่างกัน เช่นเป้าหมาย รายการซื้อของ อะไรก็ตามที่คุณสามารถใส่ลงในรายการได้
ฉันมักจะต้องการรายการ แต่ไม่มีเครื่องมือที่ฉันใช้เป็นประจำทุกวันเพื่อติดตามลิงก์ของฉัน โชคดีที่การทำเช่นนั้นด้วย React Server นั้นง่ายมาก ดังนั้นฉันจึงไม่เห็นเหตุผลที่จะไม่เขียนแอปรายการพื้นฐานสำหรับพวกคุณ
ข้อกำหนดในด้านของฉันนั้นเรียบง่าย:
* อินเทอร์เฟซที่ใช้งานง่ายน้อยที่สุด
* Google OAuth เพื่อให้สามารถใช้งานได้บนอุปกรณ์หลายเครื่อง
* สถานะคงอยู่ตลอดการโหลดไซต์ซ้ำ
ฉันมีเซิร์ฟเวอร์ตอบสนองที่ทำงานเพื่อโฮสต์เว็บไซต์สาธิตอยู่แล้ว ดังนั้นจึงสามารถวางลงในแบ็กเอนด์รายการและใช้งานได้ เซิร์ฟเวอร์ React ยังอยู่ในช่วงอัลฟ่า ดังนั้นจึงยังไม่มีการเชื่อมต่อฐานข้อมูล ซึ่งหมายความว่าข้อมูลจะหายไปหลังจากที่เซิร์ฟเวอร์รีสตาร์ท แต่สำหรับตอนนี้ ทำหน้าที่เป็น POC ที่น่าสนใจของการเข้ารหัสแบบโต้ตอบบนแบ็กเอนด์โดยใช้ TSX และ hooks
หากคุณสนใจที่จะสร้างแอป Lists ด้วยตัวเอง โปรดทำตามบทช่วยสอน Todo App เลย ฉันจะให้ลิงก์ไปยัง repo Github แก่คุณสำหรับโพสต์บนบล็อกนี้และตอนท้าย ฉันจะอธิบายการเปลี่ยนแปลงที่สำคัญสำหรับผู้อ่านโดยย่อ คุณสามารถโคลนโค้ดทั้งหมดได้จาก GitHub ซึ่งมากเกินไปสำหรับโพสต์บนบล็อกนี้
แบ็กเอนด์
ก่อนอื่นเราต้องเพิ่มการรองรับสำหรับการสร้างรายการแบบไดนามิก เพื่อทำเช่นนั้น เราจึงปรับเปลี่ยนส่วนประกอบ Todos เล็กน้อย เริ่มต้นด้วยการตั้งชื่อทั่วไปให้มากขึ้นว่า "รายการ" นอกจากนี้เรายังจำเป็นต้องเพิ่มส่วนประกอบฝั่งเซิร์ฟเวอร์อื่นที่จัดการรายการของเรา
import { Scopes, authenticate, isClientContext, useState, } from '@state-less/react-server'; import { v4 } from 'uuid'; import { ServerSideProps } from './ServerSideProps'; import { JWT_SECRET } from '../config'; type TodoObject = { id: string | null; title: string; completed: boolean; }; export const Todo = ({ id, completed, title }: TodoObject) => { const [todo, setTodo] = useState<TodoObject>( { id, completed, title, }, { key: `page${id}`, scope: Scopes.Client, } ); const toggle = () => { setTodo({ ...todo, completed: !todo.completed }); }; return <ServerSideProps key={`${id}-todo`} {...todo} toggle={toggle} />; }; export const List = (_, { key }) => { const [todos, setTodos] = useState<TodoObject[]>([], { key: 'todos', scope: `${key}.${Scopes.Client}`, }); const [title, setTitle] = useState('My List', { key: 'title', scope: `${key}.${Scopes.Client}`, }); const addEntry = (todo: TodoObject) => { const id = v4(); const newTodo = { ...todo, id }; if (!isValidTodo(newTodo)) { throw new Error('Invalid todo'); } setTodos([...todos, newTodo]); return newTodo; }; const removeEntry = (id: string) => { setTodos(todos.filter((todo) => todo.id !== id)); }; return ( <ServerSideProps key={`${key}-props`} add={addEntry} remove={removeEntry} title={title} setTitle={setTitle} > {todos.map((todo) => ( <Todo {...todo} /> ))} </ServerSideProps> ); }; export const MyLists = (_: { key?: string }, { context, key }) => { let user = null; if (isClientContext(context)) try { user = authenticate(context.headers, JWT_SECRET); } catch (e) {} const [lists, setLists] = useState([], { key: 'lists', scope: `${key}.${user?.id || Scopes.Client}`, }); const addEntry = (todo: TodoObject) => { const id = v4(); const newList = { ...todo, id }; setLists([...lists, newList]); }; const removeEntry = (id: string) => { setLists(lists.filter((list) => list.id !== id)); }; return ( <ServerSideProps key="my-lists-props" add={addEntry} remove={removeEntry} > {lists.map((list) => ( <List key={`list-${list.id}`} {...list} /> ))} </ServerSideProps> ); }; const isValidTodo = (todo): todo is TodoObject => { return todo.id && todo.title && 'completed' in todo; };
อย่างที่คุณเห็นเราได้เพิ่มส่วนประกอบ MyLists ซึ่งมีหน้าที่ในการสร้างรายการใหม่และส่งไปยังไคลเอนต์ โดยพื้นฐานแล้วตรรกะสำหรับส่วนประกอบรายการจะเหมือนกับเมื่อก่อน เราสามารถเรนเดอร์องค์ประกอบ Lists ได้หลายครั้งเพื่อนำตรรกะทางธุรกิจกลับมาใช้ใหม่ ซึ่งแสดงให้เห็นว่าแนวทางที่ขับเคลื่อนด้วยส่วนประกอบมีความยืดหยุ่นสูงและช่วยให้คุณสามารถสรุปโค้ดและพฤติกรรมของคุณโดยนำชิ้นส่วนโมดูลาร์ส่วนใหญ่กลับมาใช้ใหม่ได้อย่างง่ายดาย
นั่นคือทั้งหมดที่เราต้องจัดการกับแบ็กเอนด์ในตอนนี้ ตรงไปที่ส่วนหน้าและปรับโค้ดส่วนหน้า
ส่วนหน้า
เราไม่จำเป็นต้องทำการเปลี่ยนแปลงมากมายเพื่อแสดงรายการสิ่งที่ต้องทำหลายรายการ React บนส่วนหน้านั้นมีความยืดหยุ่นและเป็นโมดูลพอๆ กับ React Server บนแบ็กเอนด์ ดังนั้นเราจึงสามารถเรนเดอร์รายการได้หลายครั้ง
/* eslint-disable @typescript-eslint/no-misused-promises */ import { Box, Button, Card, CardHeader, Checkbox, IconButton, List as MUIList, ListItem, ListItemIcon, ListItemSecondaryAction, ListItemText, TextField, CardContent, CardMedia, CardActions, Alert, Grid, InputAdornment, Typography, } from '@mui/material'; import EditIcon from '@mui/icons-material/Edit'; import RemoveCircleIcon from '@mui/icons-material/RemoveCircle'; import { useComponent } from '@state-less/react-client'; import { useContext, useEffect, useRef, useState } from 'react'; import IconMore from '@mui/icons-material/Add'; import IconClear from '@mui/icons-material/Clear'; import { Actions, stateContext } from '../provider/StateProvider'; export const MyLists = (props) => { const [component, { loading, error }] = useComponent('my-lists', {}); const { state, dispatch } = useContext(stateContext); useEffect(() => { const onKeyUp = (e) => { console.log('KEY UP', e); if (e.key === 'z' && e.ctrlKey) { const lastAction = state.history.at(-1); console.log('LAST ACTION', lastAction); lastAction?.reverse(); dispatch({ type: Actions.REVERT_CHANGE }); } }; window.addEventListener('keyup', onKeyUp); return () => { window.removeEventListener('keyup', onKeyUp); }; }, [state]); return ( <div> {error && <Alert severity="error">{error.message}</Alert>} <Grid container spacing={1}> {component?.children?.map((list, i) => { return ( <Grid item sm={12} md={4}> <List key={list.key} list={`${list.key}`}> {' '} </List> </Grid> ); })} <Grid item sm={12} md={4}> <NewListSkeleton onAdd={() => component?.props?.add()} /> </Grid> </Grid> </div> ); }; export const NewListSkeleton = ({ onAdd }) => { return ( <Card sx={{ height: '100%' }}> <Box sx={{ display: 'flex', justifyContent: 'center', height: '100%' }}> <Box sx={{ my: 'auto' }}> <Button onClick={onAdd}> <IconMore /> </Button> </Box> </Box> </Card> ); }; export const List = ({ data, list }) => { const { dispatch, state } = useContext(stateContext); const [component, { loading, error }] = useComponent(list, {}); const [title, setTitle] = useState(''); const [edit, setEdit] = useState(false); const inputRef = useRef<HTMLInputElement | null>(null); if (loading) { return null; } const addEntry = async (e) => { setTitle(''); const res = await component.props.add({ title, completed: false, }); const id = res.id; dispatch({ type: Actions.SHOW_MESSAGE, value: `Added ${title}. Undo? (Ctrl+Z)`, }); dispatch({ type: Actions.RECORD_CHANGE, message: `Added ${title}. Undo?`, value: { reverse: () => { component.props.remove(id); }, }, }); }; return ( <Card sx={{ height: '100%' }}> {error && <Alert severity="error">{error.message}</Alert>} <CardHeader title={ <> {!edit && ( <Typography variant="h6">{component?.props?.title}</Typography> )} <Box sx={{ display: 'flex', alignItems: 'center' }}> <TextField fullWidth inputRef={inputRef} value={edit ? component?.props?.title : title} label={edit ? 'Title' : 'Add Entry'} onChange={(e) => edit ? component?.props?.setTitle(e.target.value) : setTitle(e.target.value) } onKeyDown={(e) => { if (!edit && e.key === 'Enter') { addEntry(e); } }} InputProps={{ endAdornment: ( <InputAdornment position="end"> <IconButton onClick={() => { setTitle(''); setTimeout(() => inputRef.current?.focus(), 0); }} disabled={!title} > <IconClear /> </IconButton> </InputAdornment> ), }} /> <IconButton disabled={!title} onClick={addEntry}> <IconMore /> </IconButton> </Box> </> } ></CardHeader> <MUIList> {component?.children.map((todo, i) => ( <TodoItem key={i} todo={todo.key} edit={edit} remove={component?.props?.remove} /> ))} </MUIList> <CardActions> <Button onClick={() => setTitle('') || setEdit(!edit)}> <EditIcon /> </Button> </CardActions> </Card> ); }; const TodoItem = (props) => { const { dispatch, state } = useContext(stateContext); const { todo, edit, remove } = props; const [component, { loading }] = useComponent(todo, {}); TS if (loading) return null; return ( <ListItem> {edit && ( <ListItemIcon> <IconButton onClick={() => remove(component.props.id)}> <RemoveCircleIcon /> </IconButton> </ListItemIcon> )} <ListItemText primary={component.props.title} /> <ListItemSecondaryAction> <Checkbox checked={component?.props.completed} onClick={() => { dispatch({ type: Actions.SHOW_MESSAGE, value: `Marked ${component.props.title}. Undo? (Ctrl+Z)`, }); dispatch({ type: Actions.RECORD_CHANGE, value: { reverse: () => { console.log('Reversing'); component?.props.toggle(); }, }, }); component?.props.toggle(); }} /> </ListItemSecondaryAction> </ListItem> ); };
อย่ากลัวกำแพงโค้ดอันใหญ่โต มีมากกว่า 220 บรรทัด แต่โค้ดส่วนหน้ามีแนวโน้มที่จะละเอียดกว่านี้เล็กน้อย หากคุณต้องการโครงสร้างและสไตล์ของโค้ดที่เหมาะสม
ส่วนที่เหลือ
แน่นอนว่านั่นไม่ใช่ทุกสิ่งที่จำเป็นในการรันแอปพลิเคชัน อย่างที่คุณเห็น มีตรรกะการจัดการสถานะบางอย่างที่ให้คุณกด Ctrl+Z เพื่อยกเลิกคำสั่งสุดท้ายของคุณ รหัสนี้รวมอยู่ใน repo Github ซึ่งฉันจะอยู่ที่ด้านล่างของโพสต์ คุณสามารถโคลนและรันเซิร์ฟเวอร์และส่วนหน้าในเครื่องได้อย่างอิสระ
เข้าสู่ระบบ
ข้อกำหนดประการหนึ่งคือต้องสามารถใช้บัญชี Google ของคุณเพื่อลงชื่อเข้าใช้เพื่อให้สามารถใช้แอปบนสมาร์ทโฟนและเดสก์ท็อปได้ในเวลาเดียวกัน
เห็นได้ชัดว่ามีความมหัศจรรย์ในการรับรองความถูกต้องเกิดขึ้นในโค้ดส่วนหน้าซึ่งไม่ได้แสดงไว้ที่นี่ หากคุณสนใจ คุณสามารถ "อ่านเพิ่มเติมเกี่ยวกับการรับรองความถูกต้อง" ได้ที่เอกสาร
เสร็จแล้ว
แน่นอนว่าแค่ไม่กี่ชั่วโมงนั้นไม่เพียงพอที่จะสร้างแอปเวอร์ชันที่ใช้งานจริงที่ทำงานได้เต็มรูปแบบโดยมีผู้ใช้นับพันราย แต่ฉันคิดว่ามันแสดงให้เห็นว่าคุณสามารถสร้างต้นแบบแอปพลิเคชัน fullstack ด้วยเซิร์ฟเวอร์ตอบสนองได้อย่างรวดเร็ว
หากคุณมีไอเดียเกี่ยวกับฟีเจอร์ที่มีประโยชน์ แสดงความคิดเห็นหรือ ทวีตถึงเรา (@statelesscloud)
Github Repo
แบ็กเอนด์: https://github.com/C5H8NNaO4/lists-app-backend
ส่วนหน้า:https://github.com/C5H8NNaO4/lists-app-frontend
นี่คือเวอร์ชันสดที่ https://state-less.cloud/lists
เนื้อหาเพิ่มเติมได้ที่ PlainEnglish.io.
ลงทะเบียนเพื่อรับ จดหมายข่าวรายสัปดาห์ฟรี ของเรา ติดตามเราบน Twitter, LinkedIn, YouTube และ Discord .