Skip to content

Instantly share code, notes, and snippets.

@sauldeleon
Last active September 5, 2020 12:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sauldeleon/b4b485ff96b62ac215a1f3a55749293f to your computer and use it in GitHub Desktop.
Save sauldeleon/b4b485ff96b62ac215a1f3a55749293f to your computer and use it in GitHub Desktop.
Storybook meets axios meets redux meets thunk
/**
This is my workarround to create a React Story for a component which internally dispatch multiple thunk actions
and also retrieve info from some server. This works also on making an story in which its main component wraps another redux
connected components.
*/
/**
BEHAVIOUR EXPLANATION
This component depends on an ID (fetchingDataId) which is stored in the parent component which contains many SomeComponents, so I need to mock the store before SomeComponent's story loads
Then, when a single SomeComponent loads, it renders two connected subcomponents which I dont have control from Storybook. And I want the story to render both components at the same time.
FooComponent, which API call is
- fooRequest?param1=1111&param2=bbbb
BarComponent, which API call is
- barRequest?param1=1111&param2=bbbb
So:
ParentComponent: (fetchingDataId=anotherProp='aaaa') [
- SomeComponent id=aProp='1234'(Story component)
* FooComponent (connected subcomponent)
* BarComponent (connected subcomponent)
- SomeComponent id=aProp='5678'(Story component)
* FooComponent (connected subcomponent)
* BarComponent (connected subcomponent)
- more SomeComponents...
]
*/
/**
---------------------------------------------------------------------
STORYBOOK CONFIGURATION
---------------------------------------------------------------------
*/
/**
SomeComponent.stories.js
My Storybook component definition
*/
import React from 'react'
import { storiesOf } from '@storybook/react'
import { withInfo } from '@storybook/addon-info'
import Provider from 'storybook/Provider'
import ParentComponent from './ParentComponent' //import of connected with Redux component
import SomeComponent from './SomeComponent' //import of connected with Redux component
const customActions = [
{
type: 'FOO_BAR_REQUEST',
payload: { fetchingDataId: 'aaaa' },
},
]
storiesOf('SomeComponent', module)
.addDecorator(story => <Provider customActions={customActions} story={story()} /> )
.add('default', () => <SomeComponent aProp="1234" anotherProp="aaaa" /> )
.add('parent', () => <ParentComponent aProp="1234" anotherProp="aaaa" /> )
/** Custom Provider for Storybook */
import React from 'react'
import { Provider as ReduxProvider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import reducer from 'reducers' //your combined reducers
import thunk from 'redux-thunk'
const store = createStore(reducer, applyMiddleware(thunk))
/**
Provider.js
Allow custom Storybook Provider to pass a created store or dispatch some actions before displaying the Story
*/
export default function Provider({ story, customStore, customActions }) {
const finalStore = customStore ? customStore : store
customActions &&
customActions.forEach(action => {
finalStore.dispatch(action)
})
return <ReduxProvider store={finalStore}>{story}</ReduxProvider>
}
/**
MyApi.js
In order to allow the component to make API calls, I used axios
*/
import axios from 'axios'
const MyApi = axios.create({
//use of STORYBOOK_* environment variables
baseURL: process.env.STORYBOOK_MODE === 'enabled' ? '' : '/api/custom/url/',
})
//dynamic export
export default new Promise(async $export => {
if (process.env.STORYBOOK_MODE === 'enabled') {
//dynamic import. This way, mock library wont come on final build
await import('storybook/mock').then(mock => {
mock.configureMock(MyApi)
})
} else {
MyApi.interceptors.request.use(
config => {
//...some configuration
return config
},
err => {
return Promise.reject(err)
}
)
MyApi.interceptors.response.use(null, err => {
// ... some configuration
return Promise.reject(err)
})
}
$export(MyApi)
})
/**
storybook/mock/index.js file
Here I will use axios-mock-adapter to mock all custom requests
*/
import MockAdapter from 'axios-mock-adapter'
const FOO_RESPONSE = require('storybook/mock/FOO_RESPONSE.json')
const BAR_RESPONSE = require('storybook/mock/BAR_RESPONSE.json')
// ... more mocked responses
const mocks = {
FOO_RESPONSE,
BAR_RESPONSE,
// ...more mocked responses
}
export const configureMock = api => {
const mock = new MockAdapter(api, { delayResponse: 1500 })
mock
.onGet(/.*fooRequest.*/)
.reply(200, mocks['FOO_RESPONSE'])
.onGet(/.*barRequest.*/)
.reply(200, mocks['BAR_RESPONSE'])
}
/**
my-custom-action-reducer.js
Finally, my action and my reducer with thunk applied on the App Provider config
*/
import MyApi from 'MyApi'
/**
Both API calls needs to resolve a promise first, as the MyApi dynamic export returns a promise. This promise, depending on the environment
will contain the original production API or the mocked API.
*/
const getFooData = (aProp, anotherProp) => {
return MyApi.then(api =>
api({
method: 'GET',
url: `fooRequest?param1=${aProp}&param2=${anotherProp}`,
})
)
}
const getBarData = (aProp, anotherProp) => {
return MyApi.then(api =>
api({
method: 'GET',
url: `barRequest?param1=${aProp}&param2=${anotherProp}`,
})
)
}
// Actions
export const loadFooData = (aProp, anotherProp) => {
return dispatch => {
dispatch({ type: 'FOO_DATA_REQUEST' })
return getFooData(aProp, anotherProp)
.then(response => {
dispatch({
type: 'FOO_DATA_LOADED',
payload: {
aProp,
anotherProp,
data: response,
},
})
})
.catch(err => {
dispatch({ type: 'FOO_DATA_ERROR', error: err })
})
}
}
export const loadBarData = (aProp, anotherProp) => {
return dispatch => {
dispatch({ type: 'BAR_DATA_REQUEST' })
return getBarData(aProp, anotherProp)
.then(response => {
dispatch({
type: 'BAR_DATA_LOADED',
payload: {
aProp,
anotherProp,
data: response,
},
})
})
.catch(err => {
dispatch({ type: 'BAR_DATA_ERROR', error: err })
})
}
}
const defaultState = {
error: null,
fetchingDataId: null,
fooData: {},
barData: {},
isFetchingFooData: false,
isFetchingBarData: false,
}
// reducers
/**
In order to understand reducer's behaviour, this is the state after some successfull calls in this example
{
error: null,
fetchingDataId: null,
fooData: {
'1234/aaaa': { ...serverFooData },
'5678/aaaa': { ...serverFooData }
},
barData: {
'1234/aaaa': { ...serverBarData },
'5678/aaaa': { ...serverBarData }
},
isFetchingFooData: false,
isFetchingBarData: false,
}
*/
const fooBarReducers = (state = defaultState, action) => {
switch (action.type) {
case 'FOO_BAR_REQUEST':
return {
...state,
fetchingDataId: action.payload.fetchingDataId,
}
case 'FOO_DATA_REQUEST':
return {
...state,
isFetchingFooData: true,
}
case 'FOO_DATA_LOADED':
state.fooData[
action.payload.aProp + '/' + action.payload.anotherProp
] =
action.payload.data
return {
...state,
isFetchingFooData: false,
}
case 'FOO_DATA_ERROR':
state.fooData[
action.payload.aProp + '/' + action.payload.anotherProp
] = null
return {
...state,
isFetchingFooData: false,
error: action.error,
}
case 'ORDER_BAR_DATA_REQUEST':
return {
...state,
isFetchingBarData: true,
}
case 'ORDER_BAR_DATA_LOADED':
state.barData[
action.payload.aProp + '/' + action.payload.anotherProp
] =
action.payload.data
return {
...state,
isFetchingBarData: false,
}
case 'BAR_DATA_ERROR':
state.barData[
action.payload.aProp + '/' + action.payload.anotherProp
] = null
return {
...state,
isFetchingBarData: false,
error: action.error,
}
default:
return state
}
}
export default fooBarReducers
/**
Sources:
https://medium.com/@rafaelrozon/mock-axios-storybook-72404b1d427b
https://medium.com/@WebReflection/javascript-dynamic-import-export-b0e8775a59d4
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment