はじめに
Recoilを使って表示、検索、編集を実装してみた。
State
src/atoms/app.ts
import { atom } from 'recoil'
export type App = {
search: string
openNewItemForm: boolean
}
export const appState = atom<App>({
key: 'app',
default: {
search: '',
openNewItemForm: false,
},
})
-
search
検索ボックスから入力した値を保存するのに使う -
openNewItemForm
追加するフォームが開いてるかどうかの条件を保存するのに使う
src/atoms/items.ts
import { atom } from 'recoil'
export type Item = {
id: string
title: string
body: string
}
export type ItemForm = Item & {
edit: boolean
}
export const itemsState = atom<ItemForm[]>({
key: 'itemsState',
default: [],
})
-
id
一意なid、Itemをレンダリングする時のkeyとして使う -
title
タイトル -
body
本文 -
edit
一覧に表示されてるItemが編集フォームになっているかの状態の保存に使う
表示
items.json
{
"items": [
{ "id": "2c500677-41ff-4f01-9800-f2750d4f2a08", "title": "title1", "body": "body1" },
{ "id": "7ecfbf0e-b2d0-434a-8eed-f6abcf3f7a19", "title": "title2", "body": "body2" },
{ "id": "69b291d0-bcaa-49ca-ad42-da11bb629863", "title": "title3", "body": "body3" },
{ "id": "3dcf7505-d136-44e0-849e-c357a1d85f81", "title": "title4", "body": "body4" }
]
}
src/components/Items.tsx
import React, { useEffect } from 'react'
import List from '@material-ui/core/List'
import { useRecoilState, useRecoilValue } from 'recoil'
import { itemsState, Item as ItemType } from '../atoms/items'
import { appState } from '../atoms/app'
import { Item } from './Item'
import { New } from './Item/New'
export const Items: React.FC = () => {
const [items, setItemsState] = useRecoilState(itemsState)
const app = useRecoilValue(appState)
useEffect(() => {
async function fetchItems(): Promise<void> {
const response = await fetch(process.env.API_ENDPOINT ? `${process.env.API_ENDPOINT}/items.json` : './items.json')
const json = await response.json()
setItemsState(() => json.items.map((item: ItemType) => ({ ...item, edit: false })))
}
fetchItems()
}, [])
return (
<List>
{app.openNewItemForm && <New />}
{items.map((item) => (
<Item key={item.id} item={item} />
))}
</List>
)
}
item.json
を非同期で取得したものをsetItemsState
でセットしてItem
コンポーネントが表示される
検索できるようにする
src/componets/Header.tsx
import React from 'react'
import { useRecoilState } from 'recoil'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import InputBase from '@material-ui/core/InputBase'
import { fade, makeStyles } from '@material-ui/core/styles'
import SearchIcon from '@material-ui/icons/Search'
import { appState } from '../atoms/app'
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
display: 'none',
[theme.breakpoints.up('sm')]: {
display: 'block',
},
},
search: {
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: fade(theme.palette.common.white, 0.25),
},
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(1),
width: 'auto',
},
},
searchIcon: {
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
inputRoot: {
color: 'inherit',
},
inputInput: {
padding: theme.spacing(1, 1, 1, 0),
paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('sm')]: {
width: '12ch',
'&:focus': {
width: '20ch',
},
},
},
}))
export const Header: React.FC = () => {
const classes = useStyles()
const [app, setAppState] = useRecoilState(appState)
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
classes={{ root: classes.inputRoot, input: classes.inputInput }}
inputProps={{ 'aria-label': 'search' }}
onChange={(event) => setAppState({ ...app, search: event.target.value })}
/>
</div>
</Toolbar>
</AppBar>
</div>
)
}
src/components/Items.tsx
import React, { useEffect } from 'react'
import List from '@material-ui/core/List'
import { useSetRecoilState, useRecoilValue, selector } from 'recoil'
import { itemsState, Item as ItemType } from '../atoms/items'
import { appState } from '../atoms/app'
import { Item } from './Item'
import { New } from './Item/New'
const filteredItemsState = selector({
key: 'filteredItems',
get: ({ get }) =>
get(itemsState).filter(
(item) => item.title.indexOf(get(appState).search) > -1 || item.body.indexOf(get(appState).search) > -1
),
})
export const Items: React.FC = () => {
const setItemsState = useSetRecoilState(itemsState)
const app = useRecoilValue(appState)
const filteredItems = useRecoilValue(filteredItemsState)
useEffect(() => {
async function fetchItems(): Promise<void> {
const response = await fetch(process.env.API_ENDPOINT ? `${process.env.API_ENDPOINT}/items.json` : './items.json')
const json = await response.json()
setItemsState(() => json.items.map((item: ItemType) => ({ ...item, edit: false })))
}
fetchItems()
}, [])
return (
<List>
{app.openNewItemForm && <New />}
{filteredItems.map((item) => (
<Item key={item.id} item={item} />
))}
</List>
)
}
-
Header
コンポーネントで検索ワードを入力されたら、setAppState
で保存を行う -
filteredItemsState
でselector
のget
で検索ワードに合わない条件のものをフィルターした状態でItem
コンポーネントが表示される
追加
src/components/Item/New.tsx
import React, { useState } from 'react'
import { v4 as uuid } from 'uuid'
import ListItem from '@material-ui/core/ListItem'
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import IconButton from '@material-ui/core/IconButton'
import SaveIcon from '@material-ui/icons/Save'
import TextField from '@material-ui/core/TextField'
import { useRecoilState } from 'recoil'
import { ItemForm } from '../../atoms/items'
import { itemsState } from '../../atoms/items'
import { appState } from '../../atoms/app'
export const New: React.FC = () => {
const [items, setItemsState] = useRecoilState(itemsState)
const [app, setAppState] = useRecoilState(appState)
const [item, setItem] = useState<ItemForm>({ id: uuid(), title: '', body: '', edit: true })
return (
<ListItem>
<TextField
label="title"
type="text"
value={item.title}
onChange={(event) => setItem({ ...item, title: event.target.value })}
/>
<TextField
label="body"
style={{ margin: 8 }}
type="text"
value={item.body}
onChange={(event) => setItem({ ...item, body: event.target.value })}
/>
<ListItemSecondaryAction>
{item.title && item.body && (
<IconButton
edge="end"
onClick={() => {
setItemsState([{ ...item, edit: false }, ...items])
setAppState({ ...app, openNewItemForm: false })
}}
>
<SaveIcon />
</IconButton>
)}
</ListItemSecondaryAction>
</ListItem>
)
}
- 各項目の入力された値を
setItem
で保持する -
SaveIcon
のIconButton
がクリックされた時にsetItemsState
で編集してるItem
をsetItem
で保持してる値に入れ替える
更新
src/components/Item/Edit.tsx
import React, { useState } from 'react'
import ListItem from '@material-ui/core/ListItem'
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import IconButton from '@material-ui/core/IconButton'
import SaveIcon from '@material-ui/icons/Save'
import CancelIcon from '@material-ui/icons/Cancel'
import TextField from '@material-ui/core/TextField'
import { useRecoilState } from 'recoil'
import { Item as ItemType } from '../../atoms/items'
import { itemsState } from '../../atoms/items'
type Props = {
item: ItemType
}
export const Edit: React.FC<Props> = (props) => {
const [items, setItemsState] = useRecoilState(itemsState)
const [item, setItem] = useState<ItemType>(props.item)
return (
<ListItem>
<TextField
label="title"
type="text"
value={item.title}
onChange={(event) => setItem({ ...item, title: event.target.value })}
/>
<TextField
label="body"
style={{ margin: 8 }}
type="text"
value={item.body}
onChange={(event) => setItem({ ...item, body: event.target.value })}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
onClick={() => setItemsState(items.map((i) => (i.id === props.item.id ? { ...item, edit: false } : i)))}
>
<SaveIcon />
</IconButton>
<IconButton
edge="end"
onClick={() => setItemsState(items.map((i) => (i.id === props.item.id ? { ...props.item, edit: false } : i)))}
>
<CancelIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
)
}
- 各項目の入力された値を
setItem
で保持する -
SaveIcon
のIconButton
がクリックされた時にsetItemsState
で編集してるItem
をsetItem
で保持してる値と編集のUIを閉じるためにedit
をfalse
にして入れ替える
削除
src/components/Item/Show.tsx
import React from 'react'
import { useRecoilState } from 'recoil'
import ListItem from '@material-ui/core/ListItem'
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import ListItemText from '@material-ui/core/ListItemText'
import IconButton from '@material-ui/core/IconButton'
import DeleteIcon from '@material-ui/icons/Delete'
import EditIcon from '@material-ui/icons/Edit'
import { Item } from '../../atoms/items'
import { itemsState } from '../../atoms/items'
type Props = {
item: Item
}
export const Show: React.FC<Props> = (props) => {
const [items, setItemsState] = useRecoilState(itemsState)
return (
<ListItem>
<ListItemText primary={props.item.title} secondary={props.item.body} />
<ListItemSecondaryAction>
<IconButton
edge="end"
onClick={() => setItemsState(items.map((item) => (item.id === props.item.id ? { ...item, edit: true } : item)))}
>
<EditIcon />
</IconButton>
<IconButton
edge="end"
onClick={() => {
if (confirm(`Are you sure? ${props.item.title}`)) {
setItemsState(items.filter((item) => item.id !== props.item.id))
}
}}
>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
)
}
DeleteIcon
のIconButton
をクリックした時にitem
をfilter
で取り除いた配列をsetItemsState
でセットする