Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
React/Redux Introduction
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
adwd
July 08, 2016
Programming
5.3k
17
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
React/Redux Introduction
adwd
July 08, 2016
More Decks by adwd
See All by adwd
GraphQLのあまり知られていない魅力 (スキーマの表現力編) / domain modeling with GraphQL Schema
adwd
5
6.3k
RxJSで状態を管理する / state management by RxJS
adwd
1
1.3k
Savkin先生から学んだぼくの考えた最強のAngularアプリアーキテクチャ / The best Angular application architecture
adwd
1
1.2k
create-react-app-introduction
adwd
7
2k
Other Decks in Programming
See All in Programming
Go1.27で導入されるジェネリクスメソッドでできること
mackee
0
110
AutonomyとControlのあいだ:Graflowで記述するAIエージェント協調
myui
0
120
気圧・高度・GPSを記録&可視化するアプリ「Koudo」を作った話
hjmkth
1
200
AIだと陥りがちなJakarta EE最新技術への移行時の落とし穴と解決策
tnagao7
0
110
生成AI時代にこそ効くGo | Why Go Works in the Age of Generative AI
mom0tomo
8
3.2k
Lessons from Spec-Driven Development
simas
PRO
0
190
エージェンティックRAGにAWSで入門しよう!
har1101
8
1.5k
ECSアプリログをFireLensでコスト削減しようとしたけど諦めた話 in Fargate×Node.js
akihisaikeda
2
4.2k
キャリア迷子上等 ─ "ない道"は自分で作ればいい
16bitidol
3
2k
セキュリティの専門家じゃなくてもできる。「セキュリティ意識」をアップデートして サプライチェーン攻撃への耐性を高めよう。
tk3fftk
5
740
Agentic UI
manfredsteyer
PRO
0
150
さぁV100、メモリをお食べ・・・
nilpe
0
140
Featured
See All Featured
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
610
Highjacked: Video Game Concept Design
rkendrick25
PRO
1
390
From Legacy to Launchpad: Building Startup-Ready Communities
dugsong
0
230
Leveraging Curiosity to Care for An Aging Population
cassininazir
1
270
Stop Working from a Prison Cell
hatefulcrawdad
274
21k
Rails Girls Zürich Keynote
gr2m
96
14k
Side Projects
sachag
455
43k
Conquering PDFs: document understanding beyond plain text
inesmontani
PRO
4
2.8k
Neural Spatial Audio Processing for Sound Field Analysis and Control
skoyamalab
0
330
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
What’s in a name? Adding method to the madness
productmarketing
PRO
24
4.1k
Chrome DevTools: State of the Union 2024 - Debugging React & Beyond
addyosmani
10
1.2k
Transcript
React/Redux Introduction 2016/7/8
ࣗݾհ ాխത 2015/05ʹBizreachʹస৬ɺWebܦݧ1 ٻਓݕࡧΤϯδϯελϯόΠͷ͚ࣾཧ ը໘ΛReact/ReduxͰ։ൃத https://github.com/adwd https://twitter.com/adwd118
None
None
ελϯόΠཧը໘ද 2015/5 ~ 2015/5 jQuery 2015/7 AngularJS 1 2015/12
React/Redux
Βͳ͍͜ͱ • React/Reduxͷৄ͍͠આ໌ • React vs Angular, facebook/flux vs redux
..
Δ͜ͱ • React/ReduxͰTodoΞϓϦҎ্ͷͷΛ࡞Δ ͨΊʹඞཁͳཁૉͷβοͱͨ͠આ໌ • React, Redux, react-redux, react-router •
TodoΞϓϦΛ࡞ΕΔʢؾ͕͢ΔʣΑ͏ʹͳΔ ͷ͕ΰʔϧ
Ξϯέʔτ • ϓϩάϥϛϯάɾjavascriptະܦݧ • ϓϩάϥϛϯάɾjavascriptνϣοτϫΧϧʢॳ৺ऀʣ • React/ReduxͷνϡʔτϦΞϧΛͬͨʢΖ͏ͱͨ͠ʣ • React/ReduxνϣοτσΩϧʢීஈ͔ΒͬͯΔʣ
Ξϯέʔτ̎ • ඇΤϯδχΞ • σβΠφʔ • ϑϩϯτΤϯυΤϯδχΞ • ϞόΠϧΞϓϦΤϯδχΞ •
ͦΕҎ֎ʢόοΫΤϯυͱ͔Πϯϑϥͱ͔ʣ
ίϯςϯπ • शES2015+JSX • React • Redux • ඇಉظॲཧ, redux-thunk
• React-router • ςετ
modules default import/export // MyApp.js import foo from './MyModule' //
MyModule.js export default function foo () { ... }
modules named import/export // MyApp.js import { foo, Bar, baz
} from './MyModule' // MyModule.js export function foo () { ... } export class Bar { ... } export const baz = ... ࢀর: http://www.slideshare.net/teppeis/you-dont-know-es-modules
class import MyModule, { myAdd, MyBaseClass } from './MyModule' class
MyClass extends MyBaseClass { constructor() { } add = (a, b) => { return myAdd(a, b) } show = ({ a, b }) => { const { foo, bar } = a const { baz: { box = 1 }} = b console.log(MyModule.show({ foo, bar, box })) } } ※babel plugin transform-class-properties͕ඞཁ
destructuring assignment import MyModule, { myAdd, MyBaseClass } from './MyModule'
class MyClass extends MyBaseClass { constructor() { } add = (a, b) => { return myAdd(a, b) } show = ({ a, b }) => { const { foo, bar } = a const { baz: { box = 1 }} = b console.log(MyModule.show({ foo, bar, box })) } } show = (obj) => { const a = obj.a const b = obj.b const foo = a.foo const bar = a.bar const box = b.baz.box || 1 ... } 㱻
https://facebook.github.io/react/html-jsx.html JSX class NewComponent extends React.Component { render() { return
( <div> {/* Hello world */} <div className="awesome" style={{border: '1px solid red'}}> <label htmlFor="name">Enter your name: </label> <input type="text" id="name" /> </div> <p>Enter your HTML here</p> </div> ); } } <!-- Hello world --> <div class="awesome" style="border: 1px solid red"> <label for="name">Enter your name: </label> <input type="text" id="name" /> </div> <p>Enter your HTML here</p>
JSX class NewComponent extends React.Component { render() { const todos
= ['eat', 'sleep'] return ( <div> <ul> { todos.map(item => <li>{item}</li>) } </ul> </div> ); } } <ul> <li>eat</li> <li>sleep</li> </ul>
spread operator const list = [1, 2, 3] const list2
= [ ...list, 4] // [1, 2, 3, 4] const obj = { a: 'foo', b: 1 } const obj2 = { ...obj, b: 2, c: 'bar' } // { a: 'foo', b: 2, c: 'bar' } const prop = { className: 'my-class', style: { color: ‘white’ } } const div = ( <div {...prop} > <p>foo bar</p> <div> ) // <div class="my-class" style="color: white;”> // <p>foo bar</p> // </div>
αϯϓϧϓϩδΣΫτ
ηοτΞοϓ // Node.js 5.0Ҏ߱ΛΠϯετʔϧ cd your/working/directory git clone https://github.com/adwd/react-redux-hands-on npm
install npm run dev open http://localhost:3000ɹ// ͜Μͳը໘͕දࣔ͞ΕͨΒOK!
αϯϓϧϓϩδΣΫτ • 3ͭͷTodoΞϓϦ • ReactͷΈ • React/Redux • React/Redux +
REST API 5PEPͷδϨϯϚ w 3FEVYͰෳࡶͳঢ়ଶΛཧͰ͖Δˠαϯϓϧ ʹͦΜͳෳࡶͳͷΛग़ͤͳ͍ˠ݁ہ5PEP
αϯϓϧϓϩδΣΫτ • React, Redux, React-router, Babel, ESLint, Webpackͳ Ͳ͕͍͍ײ͡ʹઃఆ͞ΕͨϘΠϥʔϓϨʔτ͔Βfork •
https://github.com/davezuko/react-redux-starter-kit • มߋ: • react/reduxαϯϓϧίʔυͷՃɺmaterial-uiͷ ՃͳͲ • forkݩେنͳॻ͖͕͑ਐΈશવҧͬͯ·͢
αϯϓϧϓϩδΣΫτߏ • src • layouts - ΞϓϦέʔγϣϯશମͰදࣔͯ͠ ͍Δόʔɺυϩϫʔίϯϙʔωϯτ • redux
- reduxͷAction, Reducer • routes - react-routerͷϧʔςΟϯά • views - React/Reduxίϯϙʔωϯτ • server/api - json api
Redux DevTools • Reduxͷ։ൃʹศརͳChrome֦ு • https://chrome.google.com/webstore/ detail/redux-devtools/ lmhkpmbekcpmknklioeibfkpmmfibljd
͜͜Ͱը໘Λ͞ΘͬͯΈΔ
ESLintͷεεϝ • javascriptͷจ๏ΛνΣοΫͯ͘͠ΕΔ • ΤσΟλͱ࿈ܞ͢Δͷ͕Φεεϝ • eslint —fix Ͱ͋Δఔͷमਖ਼ͬͯ͘ΕΔ •
νʔϜ։ൃͰίʔυϨϏϡʔָ͕ʹͳΔ
ESLintͷεεϝ • ྫɿ Atom + linter-eslint
React
React • ViewϥΠϒϥϦ • js-centric: શ෦jsͷதʹॻ͘ • ίϯϙʔωϯτࢦ
React࠷খߏ // index.html <!DOCTYPE html> <html> <body> <div id="example"></div>
<script type=“text/javascript" src=“/foo/bar/sample.js"/> </body> </html> // sample.js import React from 'react' import ReactDOM from 'react-dom' import MyComponent from './MyComponent' class Example extends React.Component { render () { return ( <div> <h1>Hello, world!</h1> <MyComponent message='Hello, world!' /> </div> ) } } ReactDOM.render( <Example />, document.getElementById('example') )
React࠷খߏ // index.html <!DOCTYPE html> <html> <body> <div id="example"></div>
<script type=“text/javascript" src=“/foo/bar/sample.js"/> </body> </html> // sample.js import React from 'react' import ReactDOM from 'react-dom' import MyComponent from './MyComponent' class Example extends React.Component { render () { return ( <div> <h1>Hello, world!</h1> <MyComponent message='Hello, world!' /> </div> ) } } ReactDOM.render( <Example />, document.getElementById('example') ) React.Component
React ίϯϙʔωϯτ import React from 'react' class Sample extends
React.Component { constructor (props) { super(props) this.state = { text: props.prefix + 'learn react' } } render () { return ( <p>{this.state.text}</p> ) } }
React ίϯϙʔωϯτ import React from 'react' class Sample extends
React.Component { constructor (props) { super(props) this.state = { text: props.prefix + 'learn react' } } render () { return ( <p>{this.state.text}</p> ) } } React.ComponentΛܧঝ͠ɺ renderϝιουΛ࣋ͭΫϥε
React ίϯϙʔωϯτ import React from 'react' class Sample extends
React.Component { constructor (props) { super(props) this.state = { text: props.prefix + 'learn react' } } render () { return ( <p>{this.state.text}</p> ) } } ֎෦͔ΒpropsΛड͚औΔ ෦ʹstateΛ࣋ͭ
import React from 'react' class MyReactComponent extends React.Component { render
() { return ( <div> <h1>This is My Component</h1> <div> {this.props.children} </div> </div> ) } } <MyReactComponent> <p>learn react</p> <p>learn flux</p> </MyReactComponent> props.children
import React from 'react' class MyReactComponent extends React.Component { render
() { return ( <div> <h1>This is My Component</h1> <div> {this.props.children} </div> </div> ) } } <MyReactComponent> <p>learn react</p> <p>learn flux</p> </MyReactComponent> props.children Reactίϯϙʔωϯτʹωετͨ͠ ཁૉprops.childrenʹೖΔ
React sample • localhost:3000/react • src/views/ReactSample/TodoList.js • src/views/ReactSample/TodoItem.js
αϯϓϧΛ͞ΘͬͯΈΔ
Components • TodoList • TodoΛstateͱͯ͠ཧ • TodoItem • TodoΛҰͭදࣔ •
TodoআϘλϯΛදࣔ 5PEP-JTU 5PEP*UFN ˣ5PEP-JTUίϯϙʔωϯτͷ TUBUFΛͦͷ··දࣔ
TodoList state { newTodo: '', todos: [ 'learn react’,
'learn flux’, 'learn redux’ ] } • DOMͷૢ࡞͠ͳ͍ɺ ঢ়ଶΛૢ࡞ͯ͠React͕ͦΕ Λඳը͢Δ
TodoList render() render () { return ( <div> <h1>React
Sample</h1> <h2>Todos</h2> <input type='text' value={this.state.newTodo} onChange={this.changeText} /> <button onClick={this.addTodo}>add todo</button> { this.state.todos.map((text, index) => ( <TodoItem index={index} onRemove={this.deleteTodoItem} key={index}> {text} </TodoItem> )) } … </div> ) }
TodoList render() render () { return ( <div> <h1>React
Sample</h1> <h2>Todos</h2> <input type='text' value={this.state.newTodo} onChange={this.changeText} /> <button onClick={this.addTodo}>add todo</button> { this.state.todos.map((text, index) => ( <TodoItem index={index} onRemove={this.deleteTodoItem} key={index}> {text} </TodoItem> )) } … </div> ) }
import React, { Component } from 'react' export default class
TodoList extends Component { constructor (props) { super(props) this.state = { newTodo: '', todos: [ 'learn react’, 'learn flux’, 'learn redux' ] } } ... } TodoList ίϯετϥΫλ
import React, { Component } from 'react' export default class
TodoList extends Component { constructor (props) { super(props) this.state = { newTodo: '', todos: [ 'learn react’, 'learn flux’, 'learn redux' ] } } ... } TodoList ίϯετϥΫλ ঢ়ଶͷॳظԽ
TodoList ΫϥεϓϩύςΟ changeText = (e) => { this.setState({ newTodo:
e.target.value }) } addTodo = () => { const newTodo = this.state.newTodo this.setState({ newTodo: '', todos: [...this.state.todos, newTodo] }) } deleteTodoItem = (index) => { this.setState({ todos: this.state.todos.filter((todo, i) => i !== index) }) }
changeText = (e) => { this.setState({ newTodo: e.target.value }) }
addTodo = () => { const newTodo = this.state.newTodo this.setState({ newTodo: '', todos: [...this.state.todos, newTodo] }) } deleteTodoItem = (index) => { this.setState({ todos: this.state.todos.filter((todo, i) => i !== index) }) } TodoList ΫϥεϓϩύςΟ this.setState stateͷࢦఆͨ͠ύε ͚ͩߋ৽͢Δ
TodoItem.js import React, { Component, PropTypes } from 'react'
export default class TodoItem extends Component { static propTypes = { index: PropTypes.number, onRemove: PropTypes.func, children: PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.node), React.PropTypes.node ]) } render() { const { index, onRemove, children } = this.props; return ( <div> {`${index + 1}`}: {children} <input type='button' value='x' onClick={() => onRemove(index)} /> </div> ); } }
TodoItem.js import React, { Component, PropTypes } from 'react'
export default class TodoItem extends Component { static propTypes = { index: PropTypes.number, onRemove: PropTypes.func, children: PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.node), React.PropTypes.node ]) } render() { const { index, onRemove, children } = this.props; return ( <div> {`${index + 1}`}: {children} <input type='button' value='x' onClick={() => onRemove(index)} /> </div> ); } } render()
TodoItem.js import React, { Component, PropTypes } from 'react'
export default class TodoItem extends Component { static propTypes = { index: PropTypes.number, onRemove: PropTypes.func, children: PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.node), React.PropTypes.node ]) } render() { const { index, onRemove, children } = this.props; return ( <div> {`${index + 1}`}: {children} <input type='button' value='x' onClick={() => onRemove(index)} /> </div> ); } } propTypes: ࣮ߦ࣌ܕνΣοΫ
ίʔυΛ͍ͬͯ͡ΈΔ • jsϑΝΠϧΛมߋɾอଘ͢ΔͱࣗಈͰө͞ΕΔͣ • Webpack Hot Reloading • localhost:3000/react •
src/views/ReactSample/TodoList.js • src/views/ReactSample/TodoItem.js
࣮ࡍʹ͍ͬͯ͡ΈΔ
React ·ͱΊ • ViewϥΠϒϥϦ • ίϯϙʔωϯτ • props, props.children •
this.state, this.setState
React ·ͱΊ • ViewϥΠϒϥϦ • ͳͷͰMVCͰ͍͏Model, Controllerʹ͋ͨΔػೳ͕ͳ͍ • this.state, this.setStateͰෳࡶͳঢ়ଶΛཧ͢Δͷ…
→ Redux • ࣭ͱ͔ʁ
Redux
Redux • FluxΞʔΩςΫνϟͷ࣮ͷҰͭ • Flux࣮facebook/flux͔reduxͷͲͪΒ͔ͱ͍͏ งғؾʁ • υΩϡϝϯτ͕ॆ࣮ http://redux.js.org •
࣮͕300ߦ͔͠ͳ͍ͷͰಡΉͷΞϦ • ΞϓϦέʔγϣϯͷঢ়ଶΛҰͭͷJSONͰදݱ͢Δ
୯ํσʔλϑϩʔ IUUQXXXTMJEFTIBSFOFU+POBT0IMTTPOVTJOHSFEVY
୯ํσʔλϑϩʔ • ViewStoreͷঢ়ଶΛඳը͢Δ͚ͩ • ঢ়ଶΛมߋ͢ΔͷAction͚ͩ • Reducer(currentState, action) => nextStateͳ
ͨͩͷؔ
Action • ΞϓϦέʔγϣϯͷঢ়ଶΛมԽͤ͞ΔΑ͏ ͳಈ࡞Λදݱͨ͠plain javascript object • ex) TodoͷՃɺAPIΞΫηεͷཁٻ/ޭ •
typeϑΟʔϧυ͕ඞཁ { "type": "ADD_TODO", "text": "Build my first Redux app" } { "type": "FETCH_USER_SUCCEEDED", "id": 1, "name": "Dan Abramov", }
Reducer • ࣮ߦ͞Εͨactionͱݱࡏͷstate͔Βɺ࣍ͷ stateΛܭࢉ͢Δ • ͨͩͷؔ • (currentState, action) =>
nextState • action + reducer === setState తͳʁ
Reducer const initalState = [] function todo(state = initalState,
action) { switch (action.type) { case "ADD_TODO": return [...state, action.text] case "DELETE_TODO": return state.filter( (text, index) => index !== action.index ) default: return state } } state: [], action: { type: “ADD_TODO”, text: “hi” } ! nextState = reducer(state, action) ! nextState: [ “hi” ]
Store • ΞϓϦέʔγϣϯͷঢ়ଶΛอ࣋͢Δ • StoreΞϓϦέʔγϣϯதʹͻͱͭ • ΞϓϦέʔγϣϯͷঢ়ଶ1ຕͷJsonʹͳΔ
Store API • createStore: (reducer) => Store • store.dispatch: ΞΫγϣϯͷ࣮ߦ
import { createStore } from 'redux' import todo from './TodoReducer' const store = createStore(todo) store.dispatch({type: 'ADD_TODO', text: 'learn react'}) store.subscribe(() => { console.log(store.getState()) // [ ‘learn react’ ] })
View • ͳΜͰΑ͍ • console.log, vue.js, Angular 1/2 … •
Reactͷ߹ɺReactͱReduxͷ࿈ܞʹ react-reduxΛ͏
react-redux
react-redux • ReactͱReduxΛͭͳ͙ • ReduxͷStoreͱReactͷprops • ReduxͷΞΫγϣϯͱReactͷΠϕϯτ
react-redux • ReactͱReduxΛͭͳ͙ • ReactίϯϙʔωϯτͷpropsʹɺRedux StoreͷstateͱdispatchΛInject͢Δ
react-redux class TodoList extends React.Component { render() { <div> <button
onClick={this.props.dispatch({ type: 'ADD_TODO', text: ‘learn react' })} /> { this.props.todo.map((text, index) => { <TodoItem key={index}> {text} </TodoItem> }) } </div> } } store.dispatch store.getState().todo
react-redux • API2͚ͭͩ • connect • Provider
react-redux: connect • Redux Storeͷstate͔Βඞཁͳ෦ΛReact ίϯϙʔωϯτͷpropsʹೖ͢ΔηϨΫλ
react-redux: connect import { connect } from 'react-redux' class
TodoList extends React.Component { render() { <div> <button onClick={this.props.dispatch({type: 'ADD_TODO', text: 'learn redux'})} /> { this.props.todo.map((text, index) => { <TodoItem key={index}>{text}</TodoItem> }) } </div> } } const connectedTodoList = connect( state => { todo: state.todo } )(TodoList) export default connectedTodoList { "todo": "...", "users": "...", "foo": "..." }
react-redux: connect import { connect } from 'react-redux' class
TodoList extends React.Component { render() { <div> <button onClick={this.props.dispatch({type: 'ADD_TODO', text: 'learn redux'})} /> { this.props.todo.map((text, index) => { <TodoItem key={index}>{text}</TodoItem> }) } </div> } } const connectedTodoList = connect( state => { todo: state.todo } )(TodoList) export default connectedTodoList store.dispatch store.getState().todo ঢ়ଶΛද͢Jsonશମ͔Βtodo ෦͚ͩΛTodoListʹInject
react-redux: Provider • connectΛݺΔΑ͏ʹ͢Δ • ReactΞϓϦέʔγϣϯͷϧʔτίϯϙʔ ωϯτΛProviderͰϥοϓ
react-redux: Provider import React from 'react' import { render
} from 'react-dom' import App from './components/App' render( <App />, document.getElementById('root') ) import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import { createStore } from 'redux' import todo from './reducers' import App from './components/App' const store = createStore(todo) render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
Reduxαϯϓϧ • http://localhost:3000/redux • src/views/ReduxSample/ReduxSample.js • src/redux/modules/todo.js
Redux DevTools • ActionͱStateΛ࣌ܥྻͰදࣔ • աڈͷStateʹߦͬͨΓͰ͖Δ
αϯϓϧΛ͞ΘͬͯΈΔ
Action (ActionCreator) // src/redux/modules/todo.js export function addTodo () {
return { type: 'ADD_TODO' } } export function removeTodo (index) { return { type: 'REMOVE_TODO', index } } export function editNewTodo (text) { return { type: 'EDIT_NEW_TODO', text } } • ͦ͜Β͡Ύ͏ʹ dispatch({ type: ‘ADD_TODO’ })Λॻ͔ͳ ͍Α͏ʹؔʹ͓ͯ͘͠ • dispatch(addTodo())
// src/redux/modules/todo.js // Initial state const initialState = { newTodo:
'', todos: [ 'learn react', 'learn flux', 'learn redux' ] } // Reducer export default function todoReducer (state = initialState, action) { switch (action.type) { case 'ADD_TODO': return { newTodo: '', todos: [...state.todos, state.newTodo] } case 'REMOVE_TODO': return { ...state, todos: state.todos.filter((todo, i) => i !== action.index) } case 'EDIT_NEW_TODO': return { ...state, newTodo: action.text } default: return state } } Reducer
Component // src/views/ReduxSample/ReduxSample.js import { addTodo, removeTodo, editNewTodo }
from ‘../../redux/modules/todo’ class ReduxSample extends Component { render () { const { todo: { newTodo, todos }, dispatch } = this.props return ( <div> <input type='text' value={newTodo} onChange={(e) => dispatch(editNewTodo(e.target.value)} /> ... </div> ) } } const ConnectedReduxSample = connect( state => ({todo: state.todo}) )(ReduxSample) export default ConnectedReduxSample
Redux, react-redux ·ͱΊ • Redux • Flux, ୯ํσʔλϑϩʔ • Action,
Reducer, Store, View • react-redux • connectͰReduxͷঢ়ଶΛpropsʹinject • ProviderͰϧʔτͷίϯϙʔωϯτΛϥοϓ • ࣭ͳͲʁ
͜͜·Ͱͷ·ͱΊ • React • Reduxɺreact-redux • TodoΞϓϦΛ࡞Δ͜ͱ͕Ͱ͖ͦ͏
͔͜͜Β • ඇಉظॲཧ • Web APIΛୟ͘Α͏ͳॲཧ • ϧʔςΟϯά • /users/1
• ςετ
ඇಉظॲཧ
ඇಉظαϯϓϧ • http://localhost:3000/redux-async • src/views/ReduxAsyncSample/ • src/modules/asyncTodo/asyncTodo.js
ඇಉظαϯϓϧ server: localhost:3000/api/ • GET todos • POST todos/add
• DELETE todos/:index source: server/api/todos.js 2 seconds delay
αϯϓϧΛ͞ΘͬͯΈΔ
ඇಉظॲཧ • ReduxͷActionʹඇಉظॲཧΛͷ͍ͤͨ • redux-thunkΛ͏ • redux-thunk: redux middlewareͷͻͱͭͰɺ promiseΛฦؔ͢ΛActionʹͤΔ
• redux middleware: reducer࣮ߦ࣌ʹ͍͍ײ ͡ʹͯ͘͠ΕΔͭ
Async Action • APIϦεΤετૹ৴ɾϨεϙϯεड৴ͷ2 ΞΫγϣϯΛ࣮ߦ͢Δ 2 seconds after request
action response action
Async Action ඇಉظΞΫγϣϯ = dispatch => promiseͳؔ // action
creator const request = () => { type: 'REQUESTING' } const receive = (data) => { type: 'RECEIVED', data } // async action creator export function apiAccess() { return dispatch => { dispatch(request()) axios.get('api/sample') .then(response => { dispatch(receive(response.data)) }) .catch(e => { // error }) } }
// action creator const request = () => { type:
'REQUESTING' } const receive = (data) => { type: 'RECEIVED', data } // async action creator export function apiAccess() { return dispatch => { dispatch(request()) axios.get('api/sample') .then(response => { dispatch(receive(response.data)) }) .catch(e => { // error }) } } Async Action ϦΫΤετૹ৴લʹ requestΞΫγϣϯ ϨεϙϯεΛड৴ͨ͠ͱ͜ΖͰ receiveΞΫγϣϯ
Async Action (Reducer) const initialState = { loading: false,
todos = [] } function asyncReducer(state = initialState, action) { switch (action.type) { case 'REQUESTING': return { loading: true, todos = [] } case 'RECEIVED': return { loading: false, todos = action.data } default: return state } }
Async Action (Component) class AsyncComponent extends React.Component { componentDidMount()
{ this.props.dispatch(fetchTodos()) } render () { <div> { this.props.todo.loading ? <p>loading...</p> : <TodoList {...this.props.todo.todos} /> } </div> } } export default connect(state => { todo: state.todo })(AsyncComponent)
Async Action (Component) class AsyncComponent extends React.Component { componentDidMount()
{ this.props.dispatch(fetchTodos()) } render () { <div> { this.props.todo.loading ? <p>loading...</p> : <TodoList {...this.props.todo.todos} /> } </div> } } export default connect(state => { todo: state.todo })(AsyncComponent) loadingͰදࣔΛΓସ͑ componentDidMountͰ APIʹϦΫΤετΛૹ৴
ඇಉظॲཧ·ͱΊ • redux-thunk • ࠷ۙredux-saga͕ਓؾΒ͍͠(?) • Netflixͷredux-observableͱ͔ • ඇಉظɾ෭࡞༻Λѻ͏Redux middlewareͷઓࠃ࣌ײ
• ΞΫηε։࢝ɾྃͰ2ճΞΫγϣϯΛ࣮ߦ • Storeͷঢ়ଶʹAPIΞΫηεঢ়ଶΛஔ͍͓ͯ͘ • ࣭ͳͲʁ
react-router
react-router • URLભҠͱReactίϯϙʔωϯτදࣔΛಉظ ͢Δ • src/routes/index.js
ओͳAPI • <Router>, <Route> • ϧʔςΟϯάΛߦ͏ • <Route path=“/users”
component={Users} /> • <Link> • <a href=“…” />ͷׂ • <Link to=“/users”>show users</Link> • withRouter • ReactίϯϙʔωϯτͷpropsʹrouterػೳΛInject • withRouter(MyComponent)
routing // src/routes/index.js import React from 'react' import {
Route, IndexRoute } from 'react-router' import CoreLayout from 'layouts/CoreLayout/CoreLayout' import HomeView from 'views/HomeView/HomeView' import ReactSample from 'views/ReactSample' import ReduxSample from 'views/ReduxSample' import ReduxAsyncSample from 'views/ReduxAsyncSample' export default (store) => ( <Route path='/' component={CoreLayout}> <IndexRoute component={HomeView} /> <Route path='react' component={ReactSample} /> <Route path='redux' component={ReduxSample} /> <Route path='redux-async' component={ReduxAsyncSample} /> </Route> )
routing // src/routes/index.js import React from 'react' import {
Route, IndexRoute } from 'react-router' import CoreLayout from 'layouts/CoreLayout/CoreLayout' import HomeView from 'views/HomeView/HomeView' import ReactSample from 'views/ReactSample' import ReduxSample from 'views/ReduxSample' import ReduxAsyncSample from 'views/ReduxAsyncSample' export default (store) => ( <Route path='/' component={CoreLayout}> <IndexRoute component={HomeView} /> <Route path='react' component={ReactSample} /> <Route path='redux' component={ReduxSample} /> <Route path='redux-async' component={ReduxAsyncSample} /> </Route> ) pathͰύεΛࢦఆ IndexRoute path=‘users/:id/edit’ ͳͲՄೳ
Component // src/views/HomeView/HomeView.js import React, { Component, PropTypes }
from 'react' import { connect } from 'react-redux' import { Link, withRouter } from 'react-router' export class HomeView extends Component { render () { const toReduxAsync = () => this.props.router.push('redux-async') return ( ... <Link to='react'><p>react sample</p></Link> <Link to='redux'><p>redux sample</p></Link> <button onClick={toReduxAsync}>redux-async sample</button> ... ) } } HomeView.propTypes = { router: PropTypes.shape({push: PropTypes.func}) } export default connect(state => state)(withRouter(HomeView))
Component // src/views/HomeView/HomeView.js import React, { Component, PropTypes }
from 'react' import { connect } from 'react-redux' import { Link, withRouter } from 'react-router' export class HomeView extends Component { render () { const toReduxAsync = () => this.props.router.push('redux-async') return ( ... <Link to='react'><p>react sample</p></Link> <Link to='redux'><p>redux sample</p></Link> <button onClick={toReduxAsync}>redux-async sample</button> ... ) } } HomeView.propTypes = { router: PropTypes.shape({push: PropTypes.func}) } export default connect(state => state)(withRouter(HomeView)) <a />ͷ͔ΘΓʹ<Link />
Component // src/views/HomeView/HomeView.js import React, { Component, PropTypes }
from 'react' import { connect } from 'react-redux' import { Link, withRouter } from 'react-router' export class HomeView extends Component { render () { const toReduxAsync = () => this.props.router.push('redux-async') return ( ... <Link to='react'><p>react sample</p></Link> <Link to='redux'><p>redux sample</p></Link> <button onClick={toReduxAsync}>redux-async sample</button> ... ) } } HomeView.propTypes = { router: PropTypes.shape({push: PropTypes.func}) } export default connect(state => state)(withRouter(HomeView)) router.push withRouterͰίϯϙʔωϯτͷ propsʹrouterΛInject
͜͜·Ͱͷ·ͱΊ • React, Redux, Redux-thunk, React-Router • தنఔͷWebΞϓϦΛߏஙͰ͖Δʢͣʣ • ςετ
• ॻ͖·͠ΐ͏
enzyme
enzyme • air-bnbReactςετϢʔςΟϦςΟ • Reactίϯϙʔωϯτςετ͕ΊͬͪΌ؆ ୯ʹॻ͚Δʂ
αϯϓϧ // tests/views/ReactSample.spec.js import React from 'react' import {
shallow } from 'enzyme' import assert from 'power-assert' import ReactSample from 'views/ReactSample' import TodoItem from 'views/ReactSample/TodoItem' describe('ReactSample', function () { it('Should include an <h1> with \'React Sample\' text.', () => { const wrapper = shallow(<ReactSample />) assert(wrapper.childAt(0).type() === 'h1') assert(wrapper.childAt(0).text() === 'React Sample') }) it('should render three items', () => { const wrapper = shallow(<ReactSample />) assert(wrapper.find(TodoItem).length === 3) }) })
shallow rendering • ωετͨ͠Reactίϯϙʔωϯτͦͷ··
shallow rendering <div> <h1>React Sample</h1> <h2>Todos</h2> <input type='text' />
<button>add todo</button> <TodoItem>{'learn react'}</TodoItem> <TodoItem>{'learn flux'}</TodoItem> <TodoItem>{'learn redux'}</TodoItem> <h3>state: </h3> <pre> ... </pre> </div> shallow(<TodoList />)
shallow rendering • ࢠίϯϙʔωϯτͷৄࡉΛແࢹͨ͠ςετ ͕ॻ͚Δ • ࢠίϯϙʔωϯτʹมߋ͕͋ͬͯյΕͳ ͍ςετ • ࢠίϯϙʔωϯτͷೖྗ·ͰΛςετ
shallow rendering it('passes text to TodoItem', () => {
const wrapper = shallow(<ReactSample />) assert.deepEqual(wrapper.state('todos'), ['learn react', 'learn flux', 'learn redux']) assert(wrapper.find(TodoItem).length === 3) assert(wrapper.find(TodoItem).at(0).props().children === 'learn react') assert(wrapper.find(TodoItem).at(1).props().children === 'learn flux') assert(wrapper.find(TodoItem).at(2).props().children === 'learn redux') })
Full rendering (mount) • DOMͷ࡞༻Λςετ͢Δ߹ • ϥΠϑαΠΫϧϝιουͷςετ • ࢠίϯϙʔωϯτϨϯμϦϯά
Full rendering (mount) it('should render todo texts', () =>
{ const wrapper = mount(<ReactSample />) assert(wrapper.find('div').at(1).text() === '1: learn react ') assert(wrapper.find('div').at(2).text() === '2: learn flux ') assert(wrapper.find('div').at(3).text() === '3: learn redux ') })
Full rendering (mount) import React from 'react' import {
mount } from 'enzyme' import { spy } from 'sinon' import assert from 'power-assert' import { ReduxAsyncSample } from 'views/ReduxAsyncSample/ReduxAsyncSample' describe('ReduxAsyncSample', () => { it('Should call fetch when componentDidMount', () => { spy(ReduxAsyncSample.prototype, 'componentDidMount'); const onFetch = spy() const wrapper = mount( <ReduxAsyncSample {...defaultProp} fetch={onFetch}/> ) assert(ReduxAsyncSample.prototype.componentDidMount.calledOnce) assert(onFetch.calledOnce) }) }) ˞XSJUF@NPVOU@UFTUϒϥϯνࢀর
·ͱΊ • React: ViewϥΠϒϥϦ • Redux: ΞϓϦέʔγϣϯͷঢ়ଶཧ • React-Redux: ReactͱReduxͷ࿈ܞ
• Redux-Thunk: ඇಉظॲཧ • React-Router: URLϧʔςΟϯά • enzyme: ReactίϯϙʔωϯτςετϢʔςΟϦςΟ
reference • http://qiita.com/advent-calendar/2014/reactjs • ҰਓReact.js Advent Calendar 2014 • एׯݹ͍͚Ͳೖʹ
• https://speakerdeck.com/koba04/the-state-of-react-dot-js-2016 • The state of React.js 2016 • React࠷৽ͷಈɺҰਓAdvent Calendar͔ΒͷΩϟονΞοϓ • https://facebook.github.io/react/docs • http://redux.js.org/ • ެࣜυΩϡϝϯτ͕ॆ࣮ • http://postd.cc/getting-started-with-tdd-in-react/ • ReactͰTDDʢςετۦಈ։ൃʣΛ࢝ΊΑ͏ : ڥߏங͔Βςετ࡞ɺػ ೳ࣮·ͰͷৄղΨΠυ • ຊʹθϩ͔ΒTDDͰTodoΞϓϦΈ͍ͨͳͷΛ࡞ΕΔ
reference • Web+DB Press vol. 87 • @teppeis͞ΜͷES2015ಛू • http://www.slideshare.net/teppeis/effective-es6
• @teppeis͞ΜʹࡢฐࣾͷࣾษڧձͰൃද͍͍ͯͨͩͨ࣌͠ͷεϥΠυ • http://qiita.com/kuy • Redux, redux-sagaͷهࣄΛࢁॻ͍͍ͯΔ@kuy͞Μ • ൃԻʮΧΠʯ͞Μ
reference • http://redux.js.org/docs/basics/UsageWithReact.html • https://medium.com/@dan_abramov/smart-and-dumb- components-7ca2f9a7c7d0 • ίϯϙʔωϯτΛContainer/ViewͰ͚Δ • https://facebook.github.io/flux/docs/flux-utils.html#best-practices
• facebook͕ڍ͛ͨfluxΞʔΩςΫνϟͰͷϕετϓϥΫςΟε • https://www.gitbook.com/book/tonyhb/redux-without-profanity/details • DockerࣾͰReduxΛͬͨϊϋΛॻ͍ͨgitbook • StoreͱαʔόʔͷσʔλΛಉظͤ͞Δ • https://speakerdeck.com/chrisui/real-world-redux • Immutable.jsΛͬͨStore, localStorageͰͷӬଓԽͳͲ • https://speakerdeck.com/joere/flowtype-with-flow • FlowtypeͰ੩తܕνΣοΫ ͭ͘ΕΔˠΩϨΠʹεέʔϧ͢Δͭ͘Γʹ͢Δ
͓ΘΓ