Hai teman-teman, jika Anda belum membaca artikel saya sebelumnya tentang mengkodekan Aplikasi Todo sederhana dengan React Server, pastikan untuk memeriksanya karena kami membuat sebagian besar kode di sekitarnya.

Jika Anda baru mengenal React Server, lihat beranda di https://state-less.cloud/

Mari kita mulai dengan idenya. Aplikasi Daftar sederhana yang memungkinkan Anda membuat Daftar sebanyak yang Anda inginkan dan menambahkan item yang dapat dicentang ke dalamnya.

Pada dasarnya kita perlu memperluas aplikasi daftar tugas agar dapat membuat daftar dengan cepat alih-alih memiliki satu daftar tugas statis.

Ini dapat berguna jika Anda ingin melacak banyak hal dalam daftar berbeda. Seperti tujuan, daftar belanjaan, apa saja yang bisa Anda masukkan ke dalam daftar.

Saya sering membutuhkan daftar tetapi saya tidak memiliki alat yang saya gunakan setiap hari untuk melacak tautan saya. Untungnya sangat mudah untuk melakukannya dengan React Server jadi saya tidak melihat alasan untuk tidak menulis aplikasi daftar dasar untuk kalian.

Persyaratan di pihak saya sederhana:
* Antarmuka Intuitif Minimal
* Google OAuth agar dapat menggunakannya di banyak perangkat.
* Status persisten di seluruh pemuatan ulang situs

Saya sudah menjalankan server reaksi untuk meng-host situs web demo sehingga server tersebut dapat langsung masuk ke backend Daftar dan menggunakannya. React Server masih dalam tahap alpha sehingga belum memiliki koneksi database yang berarti data hilang setelah server restart, namun untuk saat ini berfungsi sebagai POC pengkodean reaktif yang menarik di backend menggunakan TSX dan hooks.

Jika Anda tertarik untuk membuat sendiri Aplikasi Daftar, silakan ikuti tutorial Aplikasi Todo. Saya akan memberi Anda tautan ke repo Github untuk posting blog ini dan bagian akhir. Saya akan membahas secara singkat perubahan penting bagi pembaca. Anda dapat mengkloning kode lengkap dari github yang terlalu banyak untuk postingan blog ini.

Bagian belakang

Pertama-tama kita perlu menambahkan dukungan untuk membuat daftar dinamis. Untuk melakukannya kami sedikit memodifikasi komponen Todos. Mari kita mulai dengan memberinya nama yang lebih umum “Daftar”. Kita juga perlu menambahkan komponen sisi server lain yang mengelola daftar kita.

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;
};

Seperti yang Anda lihat, kami menambahkan komponen MyLists yang bertanggung jawab untuk membuat daftar baru dan mengirimkannya ke klien. Logika komponen Lists pada dasarnya sama dengan sebelumnya. Kita cukup merender komponen Daftar beberapa kali untuk menggunakan kembali logika bisnisnya yang menunjukkan bahwa pendekatan berbasis komponen sangat fleksibel dan memungkinkan Anda menggeneralisasi kode dan perilaku dengan menggunakan kembali sebagian besar bagian modularnya.

Hanya itu yang perlu kami tangani di backend untuk saat ini. Mari menuju ke frontend dan sesuaikan kode frontend.

Paling depan

Kita tidak perlu melakukan banyak perubahan untuk merender beberapa daftar tugas. React di frontend sama fleksibel dan modularnya dengan server react di backend sehingga kita dapat merender daftar beberapa kali.

/* 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>
  );
};

Jangan terintimidasi oleh tembok kode yang besar. Itu lebih dari 220 baris, tetapi kode frontend cenderung lebih bertele-tele jika Anda menginginkan struktur dan gaya kode yang layak.

Sisanya

tentu saja bukan itu saja yang diperlukan untuk menjalankan aplikasi. Seperti yang Anda lihat, ada beberapa logika manajemen status yang memungkinkan Anda menekan Ctrl+Z untuk membatalkan perintah terakhir Anda. Kode ini disertakan dalam repo Github yang akan saya tampilkan di bagian bawah postingan. Jangan ragu untuk mengkloning dan menjalankan server dan frontend secara lokal.

Gabung

Salah satu persyaratannya adalah dapat menggunakan akun Google Anda untuk masuk agar dapat menggunakan aplikasi di ponsel cerdas dan desktop Anda secara bersamaan.

Jadi jelas ada beberapa keajaiban otentikasi yang terjadi di kode frontend yang tidak ditampilkan di sini. Jika Anda tertarik, Anda dapat “membaca lebih lanjut tentang autentikasi” di dokumen

Selesai

Itu saja, tentu saja beberapa jam tidak cukup untuk membangun aplikasi produksi yang berfungsi penuh dengan 1000-an pengguna, tapi menurut saya ini menunjukkan bahwa Anda dapat dengan cepat membuat prototipe aplikasi fullstack dengan server reaksi.

Jika Anda memiliki ide untuk fitur yang berguna, tinggalkan komentar atau tweet kami (@statelesscloud).

Repo Github

Bagian Belakang: https://github.com/C5H8NNaO4/lists-app-backend

Bagian depan: https://github.com/C5H8NNaO4/lists-app-frontend

Berikut versi langsungnya di https://state-less.cloud/lists

Konten lainnya di PlainEnglish.io.

Daftar ke buletin mingguan gratis kami. Ikuti kami di "Twitter", "LinkedIn", "YouTube", dan "Discord" .