- 投稿日:2020-01-14T22:47:35+09:00
[React/state更新]inputタグなどの入力フォームにおけるvalue値を更新
inputタグなど、入力値を変える場合はonChangeイベントでstateを更新する必要がある
空で設定した初期値を更新し、下記のように
name:タスク名,
content:炊事
というようにvalue値を更新できるようにする方法を備忘録として残しておく。コンポーネント内でStateを定義する
初期値は空文字のため、{name:'', content:''}で設定
const [newData, setNewData] = useState({name:'', content:''});先程のnewDataを使用し、value値に使用する
そして後ほど定義するinputChangeをonChangeイベントに設定<form> <TextField label="タスク名" name="name" value={newData.name} onChange={inputChange} /> <TextField label="内容" name="content" value={newData.content} onChange={inputChange} /> <Button color="primary" variant="contained">登録</Button> </form>コンポーネント内にonChangeイベントのfunctionを定義
keyとvalueを設定し、newDataのkeyと入力フォームのname値が一致している場合にvalueに更新し、新しく作り直したオブジェクトに代入して、setNewDataで、stateの更新を行う。
function inputChange(e){ const key = e.target.name; const value = e.target.value; newData[key] = value; let data = Object.assign({}, newData); setNewData(data); }以上、let data = Object.assign({}, newData);でオブジェクトを作り直さないとstateが更新されず、再レンダーもされません。
少し詰まりました。他にもっといいやり方があるかも。。
- 投稿日:2020-01-14T17:39:23+09:00
ReactにTypeScriptを導入【Tips集】
ReactにTypeScriptを導入したときのTipsをまとめました。
一助となればこれ幸い。
※基礎編レベルになります手始め
App.tsxで書いてみる
ファイル
App.jsをApp.tsxに変更App.tsximport React, { FC } from 'react'; const App: FC = () => { const sum = (a: number, b: number): number => { return a + b; }; return ( <> <p>{sum(1, 2)}</p> </> ); }; export default App;
FCはFunctional Component型の指定Tips
interface
interface Todo { text: string; complete: boolean; name?: string; readonly x: number; }
- 型の定義をまとめる
- 関数やオブジェクト、配列をまとめることができる
- interfaceの名前の頭文字は大文字
- name?は任意のプロパティ
readonlyは読み込み専用、xを変更することができないlet hoge: Hoge = { x: 10 }; hoge.x = 5; // error参考サイト
継承
interface Todo { text: string; complete: boolean; } interface Todo2 extend Todo { tags: string[]; }
extendで型を継承string[]は配列型で配列の中身がstring外部ファイルで管理
export interface Action { type: string; payload: Hoge[]; }
- exportで外部ファイルに適用
- 複数interfaceを1つのファイルで管理可能
分割代入に適用
const { hoge }: { hoge: Data[] } = state;
: { hoge: Data[] }で変数hogeの型指定Data[]は配列型で配列の中身がinterfaceのDatauseState
パターン1
const [value, setValue] = useState<string>('');
- useStateに文字列型を指定
パターン2
interface Todo { text: string; complete: boolean; } const [todos, setTodos] = useState<Todo[]>([]);
- useStateにオブジェクト型を指定
Todo[]は配列型で配列の中身がinterfaceのTodoパターン3
interface State { notificationVisible: boolean; } const [state, setState] = useState<State>({ notificationVisible: true }); const handleClick = () => { setState({ notificationVisible: false });
- Genericsで型指定したときの書き方
propsの型指定
interface HogeProps { name: string; age: number; prefecture?: string; } const Hoge: FC<HogeProps> = ({ name, age }) => {関数に適用
interface Todo { text: string; complete: boolean; } const addTodo = (text: string): void => { const newTodos: Todo[] = [...todos, { text, complete: false }]; setTodos(newTodos); };
: voidはreturn(返り値)がないときに指定formのonSubmitのEventの型指定
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); }; <form onSubmit={handleSubmit}>
- formの
onSubmit関数の引数にReact.FormEventを指定formのonChangeのEventの型指定
const [hoge, setHoge] = useState(''); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const { value }: { value: string } = e.target; setHoge(value); }; <input type="text" value={hoge} onChange={handleChange} />
- onChangeのevent関数の引数に
React.ChangeEvent<HTMLInputElement>)を指定const { value }: { value: string }は分割代入の型指定mapで展開するときの型指定
interface Todo { text: string; complete: boolean; } <ul> { todos.map((todo: Todo, index: number) => ( <li key={index}>{todo.text}</li> )) } </ul>stateとactionに導入
import React, { createContext } from 'react'; const FETCH_DATA: string = 'FETCH_DATA'; interface State { hoge: []; } interface Hoge { name: string; age: number; } interface Action { type: string; payload: Hoge[]; } const initialState:State = { hoge: []; }; const Index = createContext<State>(initialState); const reducer = (state: State, action: Action) => { switch (action.type) { case FETCH_DATA: return { ...state, hoge: action.payload }; default: return state; } };lazyに適用
import React, { lazy } from 'react'; const Hoge = lazy<any>(() => import('./Hoge'));dispatchに適用
interface Hoge { name: string; age: number; } interface Action { type: string; payload: Hoge[]; } type Dispatch = React.Dispatch<Action> interface Foo { store: { state: State, dispatch: Dispatch } }
- storeの型を指定
Hoge[]は配列型で配列の中身がinterfaceのHoge
- 投稿日:2020-01-14T15:50:19+09:00
moxiosを使用してREST-APIのテストコードを書く
Jestでテストコードを書くときにmockを作ってテストするケースが多いですね。
この記事ではHTTPクライアントのaxiosを使ってREST-APIを実行したときのテストコードについて書きます。install
$ yarn add axios $ yarn add -D moxios // moxiosはdevDependenciesで管理React
- callback関数を書いています
- APIのdataを取得したらdispatchでpayloadにdataを渡しています
sammple.jsimport axios from 'axios'; React.useEffect(() => { hoge(foo) }, []) const hoge = async (callback) => { const response = await axios.get('http://localhost:3030'); callback(response.data); } const foo = (data) => dispatch({ type: "FUGA", payload: data });Jest
- axiosのmock「moxios」を使ってdummyのrequest/responseを記述
- test前にmoxiosをinstallして終わるとunistallする
- testコードはasync/awaitで記述
- moxios.waitのコールバック関数にrequest/responseの記述
sample.test.jsimport moxios from 'moxios'; describe('moxios tests', () => { beforeEach(() => { moxios.install(); }); afterEach(() => { moxios.uninstall(); }); test('calls hoge callback on axios response', async () => { const dummy = 'dummy'; moxios.wait(() => { const request = moxios.requests.mostRecent(); request.respondWith({ status: 200, response: dummy, }); }); const mock = jest.fn(); await hoge(mock); expect(mock).toHaveBeenCalledWith(dummy); }); });
- 投稿日:2020-01-14T13:38:17+09:00
React Redux Hooks API + Jest + TypeScript でユニットテスト
だいぶ前に書いたReact Redux Hooks API でユニットテストをたまに見てくれる方もいるようですが、Sinonに依存しているサンプルのため、ここではJestで解決する例を取り上げてみます。
また、useSelectorを複数回実行するコンポーネントをテストしたいといったケースにどう対応するかも紹介してみます。前提
- Jest: 24.9.0
- Enzyme: 3.11.0
今回テストするコンポーネントはこちらです。
Counter.tsximport React, { FC, useCallback } from 'react' import { useSelector, useDispatch } from 'react-redux' import { RootState } from 'path/to/store' import { increment } from 'path/to/actions' export type CounterSelectedState = number const Counter: FC = () => { const count = useSelector<RootState, CounterSelectedState>( ({ counter }) => counter, ) const dispatch = useDispatch() const handleClick = useCallback(() => { dispatch(increment()) }, [dispatch]) return ( <div> <span>{count}</span> <button onClick={handleClick}>+1</button> </div> ) } export default Counter※
CounterSelectedStateでエイリアスを作っているのは、後でテストを書く際に使うためですが、必要なければなくてもいいです。Jestを使ってテストを書く
まずテストしたいことを書いてみましょう。
Counter.spec.tsximport React from 'react' import { shallow } from 'enzyme' import Counter from './Counter' import { increment } from 'path/to/actions' describe('<Counter />', () => { it('dispatches increment action', () => { const wrapper = shallow(<Counter />) wrapper.find('button').simulate('click') expect(wrapper.find('span').text()).toBe('10') expect(increment).toBeCalledTimes(1) }) })このままでは当然まだ動きません。
useSelector, useDispatchのスタブ
useSelectorとuseDispatchのスタブを用意します。import React from 'react' import { shallow } from 'enzyme' + import Counter, { CounterSelectedState } from './Counter' import { increment } from 'path/to/actions' + import { useSelector, useDispatch } from 'react-redux' + jest.mock('path/to/actions') + jest.mock('react-redux') + const useSelectorMock = useSelector as jest.Mock<CounterSelectedState> + const useDispatchMock = useDispatch as jest.Mock describe('<Counter />', () => { + beforeEach(() => { + useSelectorMock.mockReturnValue(10) + useDispatchMock.mockReturnValue(jest.fn()) + }) + afterEach(() => { + jest.resetAllMocks() + }) it('dispatches increment action', () => { const wrapper = shallow(<Counter />) wrapper.find('button').simulate('click') expect(wrapper.find('span').text()).toBe('10') expect(increment).toBeCalledTimes(1) }) })Jestでは、jest.mockでモジュールを自動モックすることができます(第2引数にファクトリを指定することができますが、今回は各テストで異なる値を返したい場合に対応するため、別のアプローチをとります)。
jest.mock('react-redux')TypeScriptでテストを書いていると、
mockReturnValueがuseSelectorから生えていないので、
力技ですが下記のようにダウンキャストします。
この際、型をjest.Mock<CounterSelectedState>とすることで、返り値の型をCounterSelectedStateとして指定することができます。const useSelectorMock = useSelector as jest.Mock<CounterSelectedState> const useDispatchMock = useDispatch as jest.Mockまた今回は、アクションクリエイタが呼ばれたことをテストしているため、合わせて、下記のようにアクションクリエイターに対してもモックしておきます(
expect(increment).toBeCalledTimes(1)の部分です)。jest.mock('path/to/actions')そして、各テスト前に
useSelectorが10を返すように設定し、各テスト後にモックをリセットしておきましょう。beforeEach(() => { useSelectorMock.mockReturnValue(10) useDispatchMock.mockReturnValue(jest.fn()) }) afterEach(() => { jest.resetAllMocks() })※
jest.Mock<CounterSelectedState>としたことで、useSelectorMock.mockReturnValueでは、CounterSelectedStateを返すように補完が効いてくれます。
今回はnumberなので恩恵はあまり感じませんが、オブジェクトを返す場合などに有効です。これでテストが通ります。
各テストで異なる値を取りたい
上記では1つのテストしか存在しておらず、
useSelectorMock.mockReturnValue(10)でカウントが10固定になっていますが、異なる値を扱いたい場合が往々にして存在します。
その場合はテスト内部で直接値を指定できます。it('has count as 11', () => { useSelectorMock.mockReturnValue(11) const wrapper = shallow(<Counter />) expect(wrapper.find('span').text()).toBe('11') })複数回の
useSelectorに対応する複数の値をストアから参照したい場合に、一度の
useSelectorで複数の値をまとめたオブジェクトとして返すのではなく、
useSelectorを複数回実行し、小さな単位で取得することが推奨されています。
FYI: Call useSelector Multiple Times in Function Components
つまり、先程のコンポーネントで別のStateの値(ここではsomething.isActiveを例とします)を使う際に、export type CounterSelectedState = { count: number isActive: boolean } const count = useSelector<RootState, CounterSelectedState>( ({ counter, something }) => ({ count: counter, something: something.isActive, }), )ではなく、
export type CounterSelectedState = number const count = useSelector<RootState, CounterSelectedState>( ({ counter }) => counter, ) const isActive = useSelector<RootState, boolean>( ({ something }) => something.isActive, )が望ましいということです。
前者であれば、上記のuseSelectorMock.returnValueで対応可能ですが、
後者は複数回useSelectorを実行しているため、異なるアプローチが必要となります。色々と対応方法はありますが、簡単にいくつか紹介しておきます。
方法1:
mockReturnValueOnceを使うJestのモックには
mockReturnValueOnceメソッドが存在しており、呼び出し回数に応じて返す値をコントロールすることができます。ただし、useSelectorを呼び出す順番に依存するため、扱いづらさは残ります。test('mockReturnValueOnceを使った場合', () => { useSelectorMock .mockReturnValueOnce(11) .mockReturnValueOnce(true) })※ ライフサイクルが絡む場合のテストなどにおいて、Enzymeの
shallowではなくmountをつかってテストを行いたい場合には、mockReturnValueOnceをうまく使うことでHooksの返り値をコントロールすることができますので、こういったユースケースには有効かもしれません。方法2:
mockImplementationを使うこのケースでは、
re-ducksなどの設計パターンに見られる、Stateから値を取得するためのselectorと呼ばれる関数を用います。上記の例でいえば、useSelectorに渡しているコールバック関数がそれですが、mockImplementationで参照を比較するため、関数として定義します。ここでは下記のような実装を想定します。
export function getCount({ counter }: RootState): number { return counter } export function isSomethingActive({ something }): boolean { return something.isActive }const count = useSelector<RootState, CounterSelectedState>(getCount) const isActive = useSelector<RootState, boolean>(isSomethingActive)テストでは、
useSelector.mockImplementationを使い、引数に応じて返り値をコントロールすることができます。
つまり、useSelectorに渡された引数の関数の参照が一致する場合に、それに応じた値を返すということになります。beforeEach(() => { useSelectorMock.mockImplementation(selector => { if (selector === getCount) { return 10 } else if (selector = isSomethingActive) { return true } }) })方法3: カスタムフックを定義する
これが一番ラクです。
useSelectorMockは使わずに、useSelectorを内部で呼び出すカスタムフックを定義し、
テストではこのカスタムフックのスタブを用意することで対応します。export function useCount() { return useSelector<RootState, number>(({ counter }) => counter) } export function useIsActive() { return useSelector<RootState, boolean>(({ something }) => something.isActive) }jest.mock('path/to/hooks') const useCountMock = useCount as jest.Mock<number> const useIsActiveMock = useIsActive as jest.Mock<boolean> beforeEach(() => { useCountMock.mockReturnValue(10) useIsActive.mockReturnValue(true) useDispatchMock.mockReturnValue(jest.fn()) })
かなりシンプルな例ではありますが一助になれば幸いです。
他にも色々と方法がありますが、「おすすめのこんなやり方あるよ」という方はぜひ共有ください
- 投稿日:2020-01-14T11:40:28+09:00
Reactタグの作り方
はじめに
備忘録です。
Layoutタグを作ろうかな、と思ったときに以前のコードを見直す必要があったので、
整理とまとめておくために記述します。もっといい方法があったら教えてください。
タイプミスあったらすみません。code
Layout.jsxとUseLayout.jsxは同じ階層にあるものとします.
本当は分けた方がいいです...Typescriptにする場合はChildrenは
ReactElementをreactからimportして割り当ててあげると良い感じで行けます.
children: ReactElement<any>こんな感じで。Layout.jsx
import React, { Fragment, cloneElement } from "react"; const Layout = props => { const { title, children } = props; return ( <Fragment> <div> {title} </div> {cloneElement(children)} </Fragment> ) } export default Layout;UseLayout.jsx
import React from "react"; import Layout from "./Layout"; const UseLayout = () => ( <Layout title="Layoutです"> <div> componentを配置してね <button>ぼたんです</button> </div> </Layout> )Dialogを作りたい場合とかイベントを動的に変化させたい場合
material-uiの同じようなDialogを複数ページに適用させたい場合
ただし、イベントは変えたいよって場合
TypeScriptで書いていますが、JavaScriptにしたい場合はpropsを消して、
= ({children, title, ...}) =>を= props =>にすればOKDialog.tsx
import React, { cloneElement, ReactElement } from "react"; import Dialog from "@material-ui/core/Dialog"; import Button from "@material-ui/core/Button"; import DialogActions from "@material-ui/core/DialogActions"; import DialogContent from "@material-ui/core/DialogContent"; import DialogContentText from "@material-ui/core/DialogContentText"; import DialogTitle from "@material-ui/core/DialogTitle"; type Props = { children: ReactElement<any>, title: string, description: string, disagree: string, agree: string, action?: Function | null, } const Dialog: React.FC<Props> = ({ children, title, description, disagree, agree, action = null, }) => { const [open, setOpen] = React.useState(false); return ( <span> {cloneElement(children, {onClick: () => setOpen(true)})} <Dialog open={open} onClose={() => setOpen(false)} > <DialogTitle>{title}</DialogTitle> <DialogContent> <DialogContentText> {descriotion} <DialogContentText> </DialogContent> <DialogActions> <Button onClick={() => setOpen(false)}> {disagree} </Button> <Button onClick={() => { action !== null & action() setOpen(false); }}> {agree} </Button> </DialogActions> </Dialog> </span> ) }ちょっと解説
上位から受け取ったchildrenはコンポーネントとして
Dialog.tsxにもらえるので、<Layout> <Button>Click!</Button> </Layout>のように使用できる.
タイトル, OKボタン, NGボタン, 説明は今回は必須の引数としているので
<Layout title="test" description="OK or NG?" disagree="NGボタン" agree="OKボタン" action={() => concole.log("OK or NGが押されました")} > <Button>Click</Button> </Layout>となる.
そしてコンポーネント単位でアクションを設定したいので
actionに実行したい関数を渡してあげるとその関数を実行することが出来る.
react-reduxを使用する場合はactionにdispatchを当てて使うこともできます.
- 投稿日:2020-01-14T07:40:54+09:00
ページネーションと並び替えに対応した投稿一覧画面とAPIの実装【初学者のReact×Railsアプリ開発 第10回】
やったこと
- Reactでの投稿一覧画面の実装と並び替えに対応するためのRails APIの実装
- ラジオボタンの変更によるAPIからの投稿の取得と表示
- reduxを使った表示する投稿の状態管理
- material-ui-flat-paginationを用いたページネーションの実装
成果物
Rails APIの実装手順
route.rb: ルートの編集
route.rbRails.application.routes.draw do namespace :api, defaults: { format: :json } do namespace :v1 do get 'posts', to: 'posts#index' get 'posts_suki', to: 'posts#suki_index' get 'posts_allcount', to: 'posts#all_count_index' end end endposts_controller
- API側のページネーションの実装として、kaminariを用いています。
- ポストは1ページあたり10個ずつ返すようにしています。新着順や投票数順など、order('...')で、postsテーブルのどのカラムで並び替えするかを記述しています。
- page_lengthは、React側でページ数を何ページまで表示するかを確定させるために必要な情報です。46個の投稿なら5ページまでなど...
posts_controller.rbdef index posts = Post.page(params[:page] ||= 1).per(10).order('created_at DESC') page_length = Post.page(1).per(10).total_pages json_data = { 'posts': posts, 'page_length': page_length, } render json: { status: 'SUCCESS', message: 'Loaded posts', data: json_data} end def suki_index posts = Post.page(params[:page] ||= 1).per(10).order('suki_count DESC') page_length = Post.page(1).per(10).total_pages json_data = { 'posts': posts, 'page_length': page_length, } render json: { status: 'SUCCESS', message: 'Loaded posts', data: json_data} end def all_count_index posts = Post.page(params[:page] ||= 1).per(10).order('all_count DESC') page_length = Post.page(1).per(10).total_pages json_data = { 'posts': posts, 'page_length': page_length, } render json: { status: 'SUCCESS', message: 'Loaded posts', data: json_data} endReact実装手順
App.js
- ルートの編集です。
App.jsimport PostsList from './containers/PostsList'; <Auth> <Switch> <Route exact path="/" component={Home} /> <Route path='/create' component={Create} /> <Route path='/postslist' component={PostsList} /> </Switch> </Auth>PostsList.js
- ここでは一部のコードのみ紹介します。
- 表示させるポストなどの情報は、Redux(PostListReducer)で管理しています。
- componentdidmountで、初期描画の際に表示させるポストの取得を行っています。前回描画時の情報を保存しておくためにreduxでの状態管理を行っています。
- handleChangeらラジオボタンの変更に対応しています。
- handlePaginationClickは、ページリンクの変更に対応しています。offsetは1ページ目なら0、2ページ目をクリックしたときは10、3ページ目なら20...です。この情報で、APIで何ページ目の情報をもらうか確定させています。
Postslist.js//module import, css部分は省略 class PostsList extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.handlePaginationClick = this.handlePaginationClick.bind(this); } componentDidMount() { const { PostsListReducer } = this.props; if (PostsListReducer.selected === "新着順") { this.props.actions.getPostsList("", PostsListReducer.offset, "新着順") } else if (PostsListReducer.selected === "スキが多い順") { this.props.actions.getPostsList("_suki", PostsListReducer.offset, "スキが多い順") } else if (PostsListReducer.selected === "投票数が多い順") { this.props.actions.getPostsList("_allcount", PostsListReducer.offset, "投票数が多い順") } } handleChange(e) { if (e.target.value === "新着順") { this.props.actions.getPostsList("", 0, "新着順") } else if (e.target.value === "スキが多い順") { this.props.actions.getPostsList("_suki", 0, "スキが多い順") } else if (e.target.value === "投票数が多い順") { this.props.actions.getPostsList("_allcount", 0, "投票数が多い順") } } handlePaginationClick(offset) { const { PostsListReducer } = this.props; if (PostsListReducer.selected === "新着順") { this.props.actions.getPostsList("", offset, "新着順") } else if (PostsListReducer.selected === "スキが多い順") { this.props.actions.getPostsList("_suki", offset, "スキが多い順") } else if (PostsListReducer.selected === "投票数が多い順") { this.props.actions.getPostsList("_allcount", offset, "投票数が多い順") } }PostsList.js(render)
- 続いて、レンダーの部分です。
- PostListReducerの情報を使って、表示を制御しています。
- 各ポストには、リンク("/posts/post.id")を貼って、詳細ページに飛べるようにしています。
- RadioGroupタグと、Paginationタグの設定が多少頭を使います。
PostsList.jsrender() { const { CurrentUserReducer } = this.props; const { PostsListReducer } = this.props; const { classes } = this.props; return ( <Scrollbars> <div className={classes.container}> <FormControl component="fieldset"> <FormLabel component="legend"></FormLabel> <RadioGroup aria-label="position" name="position" value={PostsListReducer.selected} onChange={this.handleChange} row> <FormControlLabel value="新着順" control={<Radio color="primary" />} label="新着順" labelPlacement="end" /> <FormControlLabel value="スキが多い順" control={<Radio color="primary" />} label="スキが多い順" labelPlacement="end" /> <FormControlLabel value="投票数が多い順" control={<Radio color="primary" />} label="投票数が多い順" labelPlacement="end" /> </RadioGroup> </FormControl> <ul className={classes.ul}> {PostsListReducer.items.map((post) => ( <Link to={"/posts/" + post.id} className={classes.link}> <li className={classes.li} key={post.id}> <div className={classes.licontent}> <h3 className={classes.lih3}>{post.content}</h3> </div> </li> </Link> ))} </ul> <MuiThemeProvider theme={pagitheme}> <CssBaseline /> <Pagination limit={10} offset={PostsListReducer.offset} total={PostsListReducer.page_length * 10} onClick={(e, offset) => this.handlePaginationClick(offset)} /> </MuiThemeProvider> </div> </Scrollbars> ) } }actions/index.js
- ここでは、APIから投稿を取得しています。
- PostsListReducer.jsでstateを変更するためのactionの内容の記述とdispatchをしています。
index.jsexport const getPostsList = (fetchlink, offset, selected) => { return (dispatch) => { dispatch(getPostsListRequest()) const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid const page_url = offset / 10 + 1 return axios.get(process.env.REACT_APP_API_URL + `/api/v1/posts${fetchlink}?page=${page_url}`, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) .then(response => dispatch(getPostsListSuccess(response.data.data.posts, offset, response.data.data.page_length, selected))) .catch(error => dispatch(getPostsListFailure(error, offset, selected))) }; }; export const getPostsListRequest = () => ({ type: 'GET_POSTSLIST_REQUEST', }) export const getPostsListSuccess = (json, offset, page_length, selected) => ({ type: 'GET_POSTSLIST_SUCCESS', items: json, offset: offset, page_length: page_length, selected: selected, }) export const getPostsListFailure = (error, offset, selected) => ({ type: 'GET_POSTSLIST_FAILURE', items: error, offset: offset, selected: selected, })reducers/PostListReducer.js
- ここでreduxのstateの変更を行っています。
- initialStateに記述の通り、初期状態では新着順の1ページ目が表示されるようになっています。
PostListReducer.jsconst initialState = { isFetching: false, items: [], offset: 0, page_length: 1, selected: "新着順", }; const PostsListReducer = (state = initialState, action) => { switch (action.type) { case 'GET_POSTSLIST_REQUEST': return { ...state, isFetching: true, items: [], offset: "", page_length: "", }; case 'GET_POSTSLIST_SUCCESS': return { ...state, isFetching: false, items: action.items, offset: action.offset, page_length: action.page_length, selected: action.selected, }; case 'GET_POSTSLIST_FAILURE': return { ...state, isFetching: false, error: action.error, selected: action.selected, offset: action.offset, }; default: return state; } }; export default PostsListReducer;reducers/rootReducer.js
- rootReducerにPostsListReducerを追加しています。
rootReducer.jsimport { combineReducers } from 'redux' import { reducer as formReducer } from 'redux-form' import { routerReducer } from 'react-router-redux' import CurrentUserReducer from './CurrentUserReducer' import PostsListReducer from './PostsListReducer' const rootReducer = combineReducers({ CurrentUserReducer, form: formReducer, router: routerReducer, PostsListReducer }) export default rootReducer
- 投稿日:2020-01-14T03:13:14+09:00
別ドメインの画像ファイルをローカルに保存する(React+canvas)
Reactでcanvasを使用して「ボタンを押したら画像をダウンロードする機能」を実装しようとしたら詰まったので備忘録
かなりニッチなニーズaタグによる画像のダウンロード
通常aタグにdownload属性をつければファイルは簡単にダウンロードできるが
<a href="https://hoge.com/sample.png" download="saved.png">ダウンロード</a>のように別ドメインのサーバーに存在する画像を指定すると別タブで開くだけでダウンロードされない
aタグのdownload属性は同一オリジンでのみ動作するので別ドメインの画像はダウンロードできないcanvasを使用してダウンロード
canvasを使用して画像をblobに変換してダウンロードする方法
const c = document.getElementById('canvas'); c.toBlob((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement("a"); document.body.appendChild(a); a.download = 'foo.png'; a.href = url; a.click(); }, 'image/png');こんな感じでblobに変換してダウンロードすることが可能なのでこちらを試してみる
しかし、、
Tainted canvases may not be exportedこんな感じのエラーが発生
キャンバスは別オリジンの画像を入れると汚染されてしまいtoBlobなどのメソッドが使えなくなるそう
crossOrigin="anonymous"
canvas内のimgタグに
crossOrigin="anonymous"をつけると別オリジン間でのダウンロードが可能になる余計な画像のキャッシュ
crossOrigin="anonymous"をつけたらダウンロードできるようになる場合とならない場合があるcrossOrigin属性を付ける前に画像のリクエストを送っている場合、キャッシュを保存している可能性がある
特にS3に画像を保存している場合、S3は Orgin ヘッダが含まれていないとAccess-Control-系のヘッダを返さない
crossOrigin属性を付ける前のimageタグではOriginヘッダを送信せずにオリジン許可が得られずに怒られる可能性がある<img src="https://hoge.com/sample.png?cache=none" crossOrigin="anonymous" alt=""/>
のようにすればキャッシュの問題も解決できる
S3の場合はこれでキャッシュの削除ができたが他のサーバーではできないこともあったので他の方法を検討したほうが良さそうReactで画像ダウンロード機能を実装してみる
今回は2枚の画像を同時にダウンロードする機能を作成する
まずはカスタムフック
enhance.tsimport { useRef, RefObject } from 'react'; const useEnhancer = () => { const canv = useRef<HTMLCanvasElement>(null); const canv2 = useRef<HTMLCanvasElement>(null); const img = useRef<HTMLImageElement>(null); const img2 = useRef<HTMLImageElement>(null); const downloadImage = ( canvas: RefObject<HTMLCanvasElement>, image: RefObject<HTMLImageElement>, id: string ) => { if (canvas.current !== null) { const currentCnavas = canvas.current; const ctx = currentCnavas.getContext('2d'); if (ctx && image.current !== null) { const currentImage = image.current; currentCnavas.width = currentImage.width; currentCnavas.height = currentImage.height; ctx.drawImage(currentImage, 0, 0, currentImage.width, currentImage.height); } const anchor: HTMLAnchorElement = document.createElement('a'); currentCnavas.toBlob((blob: any) => { if (anchor !== null && blob) { anchor.href = window.URL.createObjectURL(blob); anchor.download = `${id}.png`; anchor.click(); } }); } }; return { downloadImage, canv, canv2, img, img2, }; }; export default useEnhancer;View
index.tsximport React from 'react'; import useEnhancer from './enhance'; const DownloadImage = () => { const enhance = useEnhancer(); return ( <div> <button onClick={() => { enhance.downloadImage(enhance.canv, enhance.img, '1'); enhance.downloadImage(enhance.canv2, enhance.img2, '2'); }} type="button" > ダウンロード </button> <canvas ref={enhance.canv} style={{ display: 'none' }}> <img ref={enhance.img} src="https://cdn.qiita.com/assets/qiita-fb-fe28c64039d925349e620ba55091e078.png?cache=none" alt="" crossOrigin="anonymous" /> </canvas> <canvas ref={enhance.canv2} style={{ display: 'none' }}> <img ref={enhance.img2} src="https://cdn.qiita.com/assets/qiita-fb-2887e7b4aad86fd8c25cea84846f2236.png?cache=none" alt="" crossOrigin="anonymous" /> </canvas> </div> ); };これにて一件落着
参考
- 投稿日:2020-01-14T01:07:47+09:00
React + TypeScriptのアプリケーションにStorybook + Storyshotsを導入する
React + TypeScriptのアプリケーションの環境を構築していて、StorybookとStoryshotsの設定でハマったところのメモ。
環境
React
16.12.0
typescript3.7.4
webpack4.41.5
storybook5.2.8最初create-react-appでやっていたが、storyshotの導入でwebpackやjestの設定を変える必要が出てきたので、使わないで構築し直した。
storybookの導入
セットアップ
package.json
devDependenciesに以下を追加してインストール
package.json"@storybook/addon-actions": "5.2.8", "@storybook/addon-links": "5.2.8", "@storybook/addons": "5.2.8", "@storybook/preset-typescript": "1.2.0", "@storybook/react": "5.2.8",scriptsにスクリプトを追加
package.json"storybook": "start-storybook -p 6006",/.storybook/config.jsを追加
/.storybook/config.jsimport { configure } from '@storybook/react'; const req = require.context('../src/', true, /\.stories\.(?:js|ts)x?$/); configure(req, module);/.storybook/webpack.config.js を追加
ストーリーをTypeScriptで書くには、
/.storybook/内にwebpack.config.jsを追加する。アプリケーションのwebpack設定を引き継ぎたい場合は
https://storybook.js.org/docs/configurations/custom-webpack-config/#using-your-existing-config
のように書くと短く書ける。ストーリーがtsを使っている場合は、
/stories/webpack.config.jsresolve: { extensions: ['.ts', '.tsx', '.js'], },の設定も引き継がないと、import文のpath解決がうまく行かなくてちょっとハマった。
stories.tsxでSomeComponent.stories.tsximport AppHeader from '..'; //同階層のindex.tsxをインポートしているのが、
Can't resolve '..'
と言われてしまう。
なので
/.storybook/webpack.config.js はこのようになった。/.storybook/webpack.config.jsconst path = require('path'); // your app's webpack.config.js const custom = require('../webpack.config.js'); module.exports = async ({ config, mode }) => { return { ...config, resolve: { ...config.resolve, extensions: custom.resolve.extensions }, module: { ...config.module, rules: custom.module.rules } }; };Storyshotsの導入
require-context-hookの設定
セットアップは
https://storybook.js.org/docs/testing/structural-testing/#using-storyshots
の通りだが、require.context
の部分でエラーが出てしまうので、require-context-hookの設定をする。
devDependenciesに追加するのはこの辺
package.json"jest": "24.9.0", "react-test-renderer": "16.12.0", "@storybook/addon-storyshots": "5.2.8", "@types/jest": "24.0.25", "babel-plugin-require-context-hook": "1.0.0",.babelrcにテスト用の設定を追加
"env": { "test": { "plugins": [ "require-context-hook" ] } }
/.storybook/config.jsのrequire.contextの行の前に以下を足す。/.storybook/config.jsif (process.env.NODE_ENV === 'test') { require('babel-plugin-require-context-hook/register')(); }【追記】 storybook 5.3.1 にアップデートしたらrequire.contextを使わない記述になっていたので、ここの設定は不要っぽい
![]()
cssや画像のインポートでのエラーを解決する
ストーリーの中でcssや画像をインポートしていると、storyshotsの実行時にエラーが出る。
Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.https://jestjs.io/docs/en/webpack#handling-static-assets
この通りにmoduleNameMapperを使用して解決できる。スナップショットファイルをストーリーごとに分ける
デフォルトでは全てのストーリーのスナップショットが1ファイルに書きだされる。
スナップショットはストーリーごとに分かれた方が圧倒的に差分が見やすいので、分けたい。
そこでmultiSnapshotWithOptionsというオプションを指定する。
https://github.com/storybookjs/storybook/tree/master/addons/storyshots/storyshots-core#multisnapshotwithoptionsoptionsStoryshots.test.jsをこのように書き換える。
Storyshots.test.jsimport initStoryshots, { multiSnapshotWithOptions, } from '@storybook/addon-storyshots'; initStoryshots({ test: multiSnapshotWithOptions(), });jestのconfigにはtransformの設定をする。
"transform": { "^.+\\.stories\\.tsx$": "@storybook/addon-storyshots/injectFileName", "^.+\\.jsx?$": "babel-jest", ".+\\.tsx?$": "ts-jest" }これで、各ストーリーのフォルダにスナップショットが作成される。
![]()
- 投稿日:2020-01-14T00:01:42+09:00
React hooks 機能紹介
Reactを使った開発をし始めたので、機能たちを紹介しようと思います。
React hooksにはいろんな機能があってとてもロジックを組みやすい
個人的な見解ではありますが、僕はそう感じました。
前バージョンの段階では、よく目にしたのは複雑だったりなんか難しそうだったり、なんか開発者向きなんでしょ?的なのがあったかと思いますが、React hooksが正式リリースして利用してみるとあれって?ってくらい使いやすいわかりやすいと思いました。巨頭と行ってもいいであろう機能を紹介します。
下記でとりあえずImportします。
import React, { useState, useEffect, useCallback } from 'react';下記が主に使用したHoooksたちになります。
useState
const [state, setState] = useState()useEffect
useEffect(() => { },[]);useMemo
const メモ名 = useMemo(() => { },[]);useCallback
const コールバック名 = useCallback(() => { },[]);useMemoとuseCallbackについては、なかなかはじめ理解にしにくいかと思いますが、ポイントはメモされた関数の返却かメモされた値の返却かなのかなと思います。個人的に腑に落ちたのは返却のタイミング(メモのタイミング)が違うってこと思ったことです。
[ ]←こちらについては、簡単に説明しますと対象です。
対象が、[a , b]のようにあるとそれぞれのタイミングで何かしらのa , bを処理できます。
くらしくは、フック API リファレンスを参考にどうぞ。簡単なまとめではありますが、以上になります。
他にも、様々な機能がありますので、ぜひ利用してみてください。


