ディレクトリ
- src
// プロジェクトによって変えればok
// 各componentはContainerとPresentationで分けるとPresentationはpropsを利用すればいいだけになる
- components // 以下各ディレクトリで使うもの
- index.ts // ContainerComponetをexport
- hooks.ts // ロジックを分離, テストできるようにする, useDispatch, useStateなど
- ${component名}Container.tsx // hooksやpropsを受け取り${component名}.tsxに渡す
- ${component名}.tsx // Presentational Component
- enums
- consts
- hooks // 共通hooks定義
- models
- ${domain名.ts}
- redux
- ${domain名}
- actions.ts // thunkのみ
- index.ts // adapter, initialState, reducer, selector, Stateの型, actions(actionとthunkActionをマージ)をexport
- module.ts // slice
- selectors.ts
- types.ts // actionのname spaceとthunkの型をexport
- router
- index.tsx // 各pagesへのrouter定義, lazyで読み込んだほうが軽い
- AuthRouterなど
- repositories
- utils
型定義(TypeScript)
下の定義を利用して上にtypeを拡張していくと、修正や拡張がしやすい
repository層
- apiのデータを表示用に加工してしまうとstoreにその値が反映されてしまう
- storeには純粋なデータを保持、各セレクターで取得時に加工するなど
エラーハンドリング
- apiClientなどでエラーハンドリングして加工してから再スローすると、他の箇所でエラーハンドリングの処理が書きやすい
- エラーハンドリングの処理を共通化できるもの
- reduxのactionで書いちゃう
- 分岐するもの
- hooks内でそれぞれハンドリング処理を書き、Componentで呼び出しするとよい
unwrapを使うとthunkの結果をそのまま受け取れる
thunk内でのエラーのスローも、rejectedWithValueもtry-catchで受けれる
- hooks内でそれぞれハンドリング処理を書き、Componentで呼び出しするとよい
try {
await dispatch(todoActions.getTodosAsync()).unwrap()
} catch (e) {
// error handling
}
* thunk内でエラー時の挙動
- エラーをスローした場合
- reducerのactionにはerrorに値が格納されてくる
- rejectedWithValueをreturnした場合
- reducerのactionにはpayloadに値が格納されてくる
useeffect
- 早期return
ref
stateが絡むhooks
custom hooks内で他のhooksのstate更新をする場合
- state更新をする関数を渡してあげないとダメ
const useCount = () => {
const [count, setCount] = useState(0)
}
redux
type
action
ActionのPayloadのtypeについて
const counterSlice = createSlice({
// ...
reducers: {
incrementByAmount(state, action) {
// ...
},
},
})
export const { incrementByAmount } = counterSlice.actions
type IncrementByAmountParam = ReturnType<typeof incrementByAmount>['payload']
thunk
型定義をexportする
selector
- stateの参照のみはselectorの切り出しをしないでいい
- 第二引数にfast-deep-equalを利用して再レンダリングを回避
- https://www.npmjs.com/package/fast-deep-equal
- *ただし、state参照の処理自体はかかる
- テストも不要
- 第二引数にfast-deep-equalを利用して再レンダリングを回避
- ロジック含む、配列処理(map, filter)などは検証や重い処理になってしまう場合があるのでselectorに切り出し
- createSelectorを利用
- inputSelectorsが返す値が前回と同じだった場合は、resultFunction を呼ぶことなく、キャッシュした前回の値を返す
- キャッシュ数は1
https://9oelm.github.io/2020-09-13--How-to-make-useSelector-not-a-disaster/
https://scrapbox.io/mrsekut-p/createSelector
https://scrapbox.io/mrsekut-p/useSelector
caseReducer
https://redux-toolkit.js.org/api/createSlice#examples
https://stackoverflow.com/questions/67792374/react-redux-alerting-best-practices
reducerの中で他のreducerをcallできる
reducers: {
addTodo: (state, action: PayloadAction<Omit<Todo, 'id' | 'isDone'>>) => {
const id =
state.todos.map((todo) => todo.id).reduce((a, b) => Math.max(a, b), 0) +
1
const updatedTodo = { ...action.payload, id, isDone: false }
const updatedTodos = state.todos.concat(updatedTodo)
state.todos = updatedTodos
const deleteTodoAction = slice.actions.deleteTodo(1)
slice.caseReducers.deleteTodo(state, deleteTodoAction) // ここ
},
また、他のsliceのactionもcallできる
uiStateのloadingとかもcomponent側ではなく、reducer側で処理を共通でかけたりもできる
const slice = createSlice({
name: 'ui',
initialState: {
loading: false,
},
reducers: {
updateLoading: (state, action: PayloadAction<{ loading: boolean }>) => {
state.loading = action.payload.loading
},
},
extraReducers: (builder) => {
builder
.addCase(todoActions.getTodosAsync.pending, (state, _action) => {
const action = slice.actions.updateLoading({ loading: true })
slice.caseReducers.updateLoading(state, action)
})
.addMatcher(isFulfilled(todoActions.getTodosAsync), (state, _action) => {
const action = slice.actions.updateLoading({ loading: false })
slice.caseReducers.updateLoading(state, action)
})
.addMatcher(isRejected(todoActions.getTodosAsync), (state, _action) => {
const action = slice.actions.updateLoading({ loading: false })
slice.caseReducers.updateLoading(state, action)
})
},
})
reselector
引数を渡す(不要かも)
※ただし、引数が変わるとstateが変わっていなくても再レンダリングかかる(キャッシュ数1なので、頻繁に再レンダリングされちゃいそう)
これがわかりやすい
こっちには普通に書いてるけどダメっぽい
custom hooks
useref
コンポーネントのDOMにセットしたrefをhooks内で使用する場合
- hooks内でrefを作成してそれをreturnしてあげる
- 利用側はhooksから受けたrefをセットしてあげればOK
メリット
利用側が毎回refを生成しないで済む
useref vs callback ref
callback ref
React が ref を DOM ノードにアタッチまたはデタッチするときにコードを実行したい場合は、callback refを使うといい
- refはcurrentが更新されても通知を受けれない
- callback refを使うと後から、クリックなどでrefに指定した要素が後から表示された場合でも変更を受け取ることができる
ここ読んでみる
https://www.robinwieruch.de/react-ref
test
custom hooks
- 非同期処理は、react-testing-libraryのwaitForを使うこと
使わないと、処理が終わる前にexpectが走ってしまう
tips
selectのoptionなどの値はconst定義
export const Role = {
USER: {
code: 1,
label: 'ユーザー'
},
ADMIN: {
code: 2,
label: '管理者'
},
OWNER: {
code: 3,
label: 'オーナー'
},
UNKNOWN: {
code: 0,
label: ''
}
} as const;
export type RoleKey = keyof typeof Role;
export type RoleValue = typeof Role[RoleKey];
export const findRoleByCode = (
code: number
): RoleValue =>
Object.values(Role).find(p => p.code === code) ||
Role.UNKNOWN;
利用側では
Object.values(Role)
で[{code:"",label:""}]がとれる
DFlex
import * as React from 'react';
import styled from '@emotion/styled';
import { css } from '@emotion/react';
type BoxProps = {
children: React.ReactNode;
direction?: string;
justify?: string;
align?: string;
} & React.InputHTMLAttributes<HTMLDivElement>;
const Wrapper = styled.div<Omit<BoxProps, 'children'>>`
display:flex;
${props => css`
flex-direction: ${props.direction ?? 'row'};
`}
${props =>
css`
align-items: ${props.align ?? 'center'};
`}
${props => css`
justify-content: ${props.justify ?? 'start'};
`}
`;
export type DFlexProps = BoxProps;
export const DFlex: React.VFC<DFlexProps> = React.memo(
({ children, ...props }) => {
return <Wrapper {...props}>{children}</Wrapper>;
}
);
DFlex.displayName = 'DFlex';
※reactのcomponentだと型エラーでcomponent selectorが使えないので、atomはmemoでwrapしない
https://qiita.com/kotarella1110/items/a243c7bde41fad23343f
react-tooltip
offsetと吹き出しの矢印の位置変更
<StyledTooltip
tooltipId={'testId'}
html={true}
width={`${TOOLTIP_WIDTH}px`}
offset={{ right: TOOLTIP_WIDTH / 2 - TOOLTIP_ARROW_WIDTH }}
/>
const StyledTooltip = styled(Tooltip)`
&:after {
/* 三角吹き出しの位置調整 */
left: ${TOOLTIP_ARROW_WIDTH}px !important;
}
`;