Skip to content

Instantly share code, notes, and snippets.

@josefdlange
Created September 21, 2016 21:04
Show Gist options
  • Select an option

  • Save josefdlange/dd68023971e817432976d15ed1e89261 to your computer and use it in GitHub Desktop.

Select an option

Save josefdlange/dd68023971e817432976d15ed1e89261 to your computer and use it in GitHub Desktop.
Redux REST Example
// Actions
var startLoadingX = function() {
return {
type: START_LOADING_X // Define this above as you do with Actions.
}
}
var completeLoadingX = function(response, error) {
return {
type: COMPLETE_LOADING_X, // Ditto.
response,
error
}
}
var fetchX = function(possibleParameters) {
return dispatch => {
dispatch(startLoadingX())
myApiClientIWroteThatReturnsPromisesForApiCalls.getStuff(possibleParameters)
.then(response => dispatch(completeLoadingX(response, null)))
.catch(error => dispatch(completeLoadingX(null, error)))
}
}
// Reducer
const initialState = {
isLoadingX: false,
X: {}, // or [] I suppose, depending on the shape you're expecting your response to have!
errorLoadingX: false,
XErrorData: null
}
export default function myStoresReducer(state = initialState, action) {
switch(action.type) {
case START_LOADING_X:
return Object.assign(state, {}, {
isLoadingX: true,
errorLoadingX: false,
X: {}, // you can decide if you want to clear the actual state on every load. For the sake of UI sometimes it's nice to keep old data there to show anything but a blank component.
XErrorData: null
})
case COMPLETE_LOADING_X:
if(action.error != null) {
return Object.assign(state, {}, {
isLoadingX: false,
errorLoadingX: true,
X: {}, // Or null or you can leave it alone as discussed above.
XErrorData: action.error
})
} else {
return Object.assign(state, {}, {
isLoadingX: false,
errorLoadingX: false,
X: state.response, // If your data is embedded in a generic response data structure, this would be a good place to unwrap it, or even in the API client itself.
XErrorData: null
})
}
}
}
@josefdlange

Copy link
Copy Markdown
Author

So, this is the general form I use. It's a little clunky, and there are probably solutions out there that do better, but it has worked well enough for me.

The real key is Thunk (https://github.com/gaearon/redux-thunk), which allows you to return a function instead of an object as a Redux action. This function should accept the "global" Redux dispatch function, so that when the outer action is processed, other actions can be dispatched as well from inside, including work done within a Promise's resolution.

Once you have the actions, the state shape, and the reducer implemented, you can just connect the state to whatever component needs it, and that component can dispatch the startLoadingX action when it needs to load, and any necessary state updates that happen as a result, happen at resolution of the API call's promise.

@josefdlange

Copy link
Copy Markdown
Author

Another note: I use Axios (https://github.com/mzabriskie/axios) to implement a lightweight REST client for the API I work with. It's pretty simple, and does a good job of staying out of the way.

@benz2012

benz2012 commented Sep 21, 2016

Copy link
Copy Markdown

Haha I use thunk and axios as well!

So lets say the data being returned is something like:

[
    {id: 1, item: 'Shirt', tags: []},
    {id: 2, item: 'Pants', tags: []}
]

Then you have a react component with functionality to add a tag to an element. And a complementary reducer to update the Redux store on that action.

case ADD_TAG:
    return {
        ...state, 
        tags: [...state, action.payload]
    }

Would that react component be dispatching multiple actions to keep the backend in sync with the Redux store?

addTag() {
    this.props.dispatch(addTagAction());
    this.props.dispatch(postX());
    this.props.dispatch(fetchX());
}

And I realize you would want the axios.post to chain with .then() to a subsequent axios.get to eliminate race conditions, but I wrote it like that to demonstrate my question properly.

@josefdlange

Copy link
Copy Markdown
Author

A well-implemented RESTful API will return the updated model on response, so you'd probably have in your reducer for receiving the response some code to create a new version of the state with an almost exact copy of the prior state -- except the new model data overwriting the old local version in that array. For example:

return Object.map(state, {}, {
    X: state.X.map(obj => obj.id == newObj.id ? newObj : obj)
})

@benz2012

Copy link
Copy Markdown

Wow, I'm such an idiot. Of course POST requests return the the updated model.

So you would only need one action handler -> that would dispatch a multiple-action thunk function -> which would cause the Redux store to be updated twice?

Once when the reducer catches the initial action, and again when(if) the response returns? But because the data should be identical there would be no change to the user because the React Virtual DOM will not notice any changes?

So for example:

// React Component
incrementValue() {
    this.props.dispatch(incrementValueAction(this.props.value + 1)); // value received from store via connect
}
render() {
    <button onClick={this.incrementValue.bind(this)}>Increment Value</button>
}

// Action Handler
export default incrementValueAction(value) {
    return dispatch => {
        dispatch( {type: INC_START, payload: value} )
        axios.post("rest/url/", {value: value})
            .then(dispatch( {type: INC_COMPLETE, payload: response.data} ))
            .catch(...)
    }
}

// Reducer
switch (action.type) {
    case INC_START:
        return {...state, value: action.payload}
    case INC_COMPLETE:
        return {...state, value: action.payload}
}

So then the question becomes, is it really worth having TWO reducer cases that update the Redux state to the same previous state if the RESTful database is always in sync?

@josefdlange

Copy link
Copy Markdown
Author

You don't even need the first private action (of the two inside the thunk) unless you want to be keeping track of whether or not you are loading --- so that your UI can display that and/or if you want/need to do some cleanup before receiving a potential response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment