はじめに

ここで使われている技術

画面遷移React Router
URLパラメータの取り扱い/article/123 で 123番の記事を表示する
画面間でのデータ共有Recoil
入力フォームReact Hook Form

Recoil AtomFamily?

https://github.com/kagyuu/ReactExam/blob/main/04_react_router/my-router/src/logic/state.js

import {atom, atomFamily, selector, selectorFamily} from 'recoil';

// CAUTION: Don't miss spell of "key" and "default".
//
// If you incorrectly type dafault as "deault", the whole app will stop working (the browser shows white screen)
// and Recoil reports an incorrect following error message : 
// "A component suspended while responding to synchronous input. 
// This will cause the UI to be replaced with a loading indicator. 
// To fix, updates that suspend should be wrapped with startTransition."

export const counterAtom = atom({
    key: 'counterAtom',
    default: 0
});

export const idsAtom = atom({
    key: 'idsAtom',
    default: []
});

export const articleAtom = atomFamily({
    key: 'articleAtom',
    default: null
});

export const artilceSelector = selectorFamily({
  key: "artilceSelector",
  get:  (id) => ({ get }) => {
      const atom = get(articleAtom(id));
      return atom;
  },
  set: (id) => ({set}, item) => {
    set(articleAtom(id), item);
    set(idsAtom, ids => {
        if (ids.includes(id)) {
            return ids;
        }
        return [...ids, id];
    });
  }
});

export const articleListSelector = selector({
    key: 'articleListSelector',
    /** 
     * Get all articles.
     * @param get a function to get atom.
     */
    get: ({get}) => {
        const ids = get(idsAtom);
        return ids.map(id => get(articleAtom(id)));
    },
    /** 
     * Set all articles.
     * @param get a function to get atom.
     * @param set a function to sett atom.
     * @param reset a function to reset atom.
     */
    set: ({get, set, reset}, item) => {
        console.log(item);
        set(articleAtom(item.id), item);
        set(idsAtom, ids => [...ids, item.id]);
    }
});

React Router での URLパラメータ

ルーティング設定

https://github.com/kagyuu/ReactExam/blob/main/04_react_router/my-router/src/App.js

import './App.css';
import {RouterProvider, Route, createBrowserRouter, createRoutesFromElements} from 'react-router-dom';
import './index.css';
import BaseLayout from './BaseLayout';
import TopPage from './TopPage';
import ArticlePage from './ArticlePage';
import EditPage from './EditPage';
import AboutPage from './AboutPage';
import NotFoundPage from './NotFoundPage';

const routes = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<BaseLayout/>}>
      <Route path="" element={<TopPage/>}/>
      <Route path="article/:id" element={<ArticlePage/>}/>
      <Route path="about" element={<AboutPage/>}/>
      <Route path="edit/:id" element={<EditPage/>}/>
      <Route path="*" element={<NotFoundPage/>}/>
    </Route>
  )
  // 第二引数を省略すると React Router の URL は、サイト直下 https://example.com/article になる
  // 第二引数で、ベースURLを指定すると basename下 https://example.com/myapp/article になる
  //, {basename: '/myapp'}
)

function App() {
  return (
    <>
      <RouterProvider router={routes} />
    </>
  );
}

export default App;

リンクの作成

https://github.com/kagyuu/ReactExam/blob/main/04_react_router/my-router/src/MenuPage.js

import {NavLink} from 'react-router-dom';
import {useRecoilValue} from "recoil";
import {counterAtom} from "./logic/state";
import {articleListSelector} from "./logic/state";

export default function MenuPage() {
    const count = useRecoilValue(counterAtom);
    const articleList = useRecoilValue(articleListSelector);

    return (
        <>
            <ul>
                <li><NavLink to="/">Top</NavLink></li>
                <li><NavLink to="/about">About us</NavLink></li>
                <li><NavLink to="/edit/0">New Page</NavLink></li>
                {articleList.map(article => (
                    <li key={article.id}>
                        <NavLink to={`/article/${article.id}`}>
                            {article.title.substring(0, Math.min(article.title.length, 10))}
                        </NavLink>
                    </li>
                ))}
                <li><NavLink to="/error/123">Error Page</NavLink></li>
            </ul>
            <p align="center">COUNTER: {count}</p>
        </>
    );
}

パラメータの受け取り

https://github.com/kagyuu/ReactExam/blob/main/04_react_router/my-router/src/ArticlePage.js

import {Link, useParams} from 'react-router-dom';
import {artilceSelector} from "./logic/state";
import {useRecoilValue} from "recoil";

export default function ArticlePage () {
    const {id} = useParams();
    const article = useRecoilValue(artilceSelector(parseInt(id)));

    return (
        <>
          Article #{id}. <br/>
          <h1>{article.title}</h1>
          <pre>{article.subject}</pre>
          <br/>
          <Link to={`/edit/${id}`}>Edit</Link>
        </>
    );
}

React Hook Form の入力フォーム

https://github.com/kagyuu/ReactExam/blob/main/04_react_router/my-router/src/EditPage.js

import {useParams, useNavigate} from 'react-router-dom';
import {useForm} from 'react-hook-form';
import {useRecoilState, useRecoilValue} from "recoil";
import {idsAtom, artilceSelector} from "./logic/state";

export default function EditPage () {
    const {id} = useParams();
    const navigate = useNavigate();
    const ids = useRecoilValue(idsAtom);
    const newId = ('0' === id) ? Math.max(...(ids.length ? ids : [0])) + 1 : parseInt(id);
    const [article, setArticle] = useRecoilState(artilceSelector(newId));

    const defaultValues = {
      title: article?.title ?? '',
      subject: article?.subject ?? '',
    };

    const {register, handleSubmit, reset, formState: {errors}} = useForm({
      defaultValues
    });

    const onSuc = (data, event) => {
      const submitButton = event.nativeEvent.submitter?.name;
      if ('button-cancel' === submitButton) {
        onCancel();
        return;
      }

      setArticle({
        id : newId,
        title: data.title,
        subject: data.subject
      });

      navigate('/article/' + newId);
    };
    
    const onErr = (err, event) => {
      const submitButton = event.nativeEvent.submitter?.name;
      if ('button-cancel' === submitButton) {
        onCancel();
        return;
      }
    };

    const onCancel = () => {
      reset(defaultValues);
    };

    // MEMO: ...register() の ... は可変引数。register() は、配列を返す
    return (
        <form onSubmit={handleSubmit(onSuc, onErr)} noValidate>
          <div>
            <label htmlFor="title">TITLE:</label>  
            <span className="errorMsg">{errors.title?.message}</span>
            <br/>
            <input id="title" name="title" type="text" size="20"
            {...register('title',{
              required: 'titleは必須です',
              maxLength: {
                value: 20,
                message: 'titleは20文字以内にしてください'
              }
            })}
            />
          </div>
          <div>
            <label htmlFor="subject">SUBJECT:</label>  
            <span className="errorMsg">{errors.subject?.message}</span>
            <br/>
            <textarea id="subject" name="subject" rows="25" cols="80"
            {...register('subject',{
              required: 'subjectは必須です',
            })}
            />
          </div>
          <div>
            <button type="submit" name="button-register">Register</button>
             
            <button type="submit" name="button-cancel">Cancel</button>
          </div>
        </form>
    );
}

パラメータの受け取り

Articleの取得/初期化

const ids = useRecoilValue(idsAtom);
const newId = ('0' === id) ? Math.max(...(ids.length ? ids : [0])) + 1 : parseInt(id);
const [article, setArticle] = useRecoilState(artilceSelector(newId));

Reach Hook Form の初期化'

const defaultValues = {
  title: article?.title ?? '',
  subject: article?.subject ?? '',
};

const {register, handleSubmit, reset, formState: {errors}} = useForm({
  defaultValues
});

Form

return (
        <form onSubmit={handleSubmit(onSuc, onErr)} noValidate>
          <div>
            <label htmlFor="title">TITLE:</label>  
            <span className="errorMsg">{errors.title?.message}</span>
            <br/>
            <input id="title" name="title" type="text" size="20"
            {...register('title',{
              required: 'titleは必須です',
              maxLength: {
                value: 20,
                message: 'titleは20文字以内にしてください'
              }
            })}
            />
          </div>
          <div>
            <label htmlFor="subject">SUBJECT:</label>  
            <span className="errorMsg">{errors.subject?.message}</span>
            <br/>
            <textarea id="subject" name="subject" rows="25" cols="80"
            {...register('subject',{
              required: 'subjectは必須です',
            })}
            />
          </div>
          <div>
            <button type="submit" name="button-register">Register</button>
             
            <button type="submit" name="button-cancel">Cancel</button>
          </div>
        </form>
    );

HTML#React


添付ファイル: filer7.png 95件 [詳細] filer4.png 95件 [詳細] filer6.png 91件 [詳細] filer5.png 93件 [詳細] filer3.png 97件 [詳細] filer2.png 101件 [詳細] filer1.png 96件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS   sitemap
Last-modified: 2024-02-08 (木) 23:39:38 (78d)
Short-URL: https://at-sushi.com:443/pukiwiki/index.php?cmd=s&k=d926e61e40
ISBN10
ISBN13
9784061426061