Привет, ребята, если вы не читали мою предыдущую статью о программировании простого приложения Todo с помощью React Server, обязательно ознакомьтесь с ней, так как мы строим большую часть кода вокруг нее.
Если вы новичок в React Server, посетите домашнюю страницу по адресу https://state-less.cloud/.
Начнем с идеи. Простое приложение для работы со списками, которое позволяет создавать столько списков, сколько вы хотите, и добавлять в них проверяемые элементы.
В основном нам нужно расширить приложение списка задач, чтобы иметь возможность создавать списки на лету вместо одного статического списка задач.
Это может быть полезно, если вы хотите отслеживать несколько вещей в разных списках. Например, цели, списки покупок, все, что вы можете поместить в список.
Мне часто нужны списки, но у меня нет инструмента, который бы я использовал ежедневно для отслеживания своих ссылок. К счастью, это очень легко сделать с React Server, поэтому я не видел причин не писать для вас базовое приложение для работы со списками.
Требования с моей стороны просты:
* Минимальный интуитивно понятный интерфейс
* Google OAuth для возможности использования на нескольких устройствах.
* Постоянные состояния при перезагрузке сайта.
У меня уже есть работающий сервер для размещения демонстрационного веб-сайта, поэтому он может просто подключиться к бэкэнду Lists и использовать его. React Server все еще находится на стадии альфа-тестирования, поэтому у него еще нет подключения к базе данных, что означает, что данные исчезают после перезапуска сервера, но на данный момент он служит интересным POC реактивного кодирования на бэкэнде с использованием TSX и хуков.
Если вы заинтересованы в создании приложения «Списки» самостоятельно, следуйте руководству по приложению Todo. Я дам вам ссылку на репозиторий 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 в основном такая же, как и раньше. Мы можем просто визуализировать компонент Lists несколько раз, чтобы повторно использовать его бизнес-логику, которая показывает, что подход, основанный на компонентах, очень гибкий и легко позволяет вам обобщать ваш код и поведение, повторно используя большинство его модульных частей.
Это все, что нам нужно обработать на бэкэнде на данный момент. Давайте перейдем к интерфейсу и адаптируем код интерфейса.
Внешний интерфейс
На самом деле нам не нужно делать много изменений, чтобы отобразить несколько списков задач. React на внешнем интерфейсе такой же гибкий и модульный, как и сервер React на бэкэнде, поэтому мы можем просто отображать списки несколько раз.
/* 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, чтобы отменить последнюю команду. Этот код включен в репозиторий Github, который я покажу внизу поста. Не стесняйтесь клонировать и запускать сервер и интерфейс локально.
Авторизоваться
Одно из требований заключалось в том, чтобы иметь возможность использовать свою учетную запись Google для входа в систему, чтобы иметь возможность одновременно использовать приложение на смартфоне и компьютере.
Таким образом, очевидно, что во внешнем коде также происходит какая-то магия аутентификации, которая здесь не показана. Если вам интересно, вы можете узнать больше об аутентификации в документации.
Сделанный
Вот и все, конечно, нескольких часов недостаточно, чтобы создать полностью работающее производственное приложение с тысячами пользователей, но я думаю, что это показывает, что вы можете довольно быстро создавать прототипы приложений полного стека с сервером реагирования.
Если у вас есть идеи для полезных функций, оставьте комментарий или твитните нас(@statelesscloud).
Репозиторий Github
Бэкенд: 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.