How can a ajax call fit into Redux paradigm?
- State: The state will be updated after ajax call is over. No problem.
- Reducer: Reducer will change the state when ajax call is over. No problem.
- Action: Action can be used to define "Ajax call over"
But where to put the logic of really doing an ajax call?
None of the 3. It should be a plain function, which will dispatch "Ajax call over" action when Ajax fetching is done. But since people are so obesessed with Redux’s paradigm, this ajax calling logic is defined as a SPECIAL ACTION
- It normally doesn’t have an action type, and it is not handled by reducers
- But it can be "dispatched"
- It’s not a data structure any more, but a function itself.
But how can a action be a function? That’s where the middleware "Thunk" comes into, which allows you to write action creators that return a function instead of an action
Here is an example. Before you go into it, you must know normally
- Ajax call success and Ajax call failure will be defined as different actions
- "Ajax call started" is also an action, since it will change the state in terms of "showing loading spinner"
//register the middleware
const rootStore = createStore(reducer,
applyMiddleware(thunkMiddleware) //allows you to write action creators that return a function instead of an action
);
//"real" actions and their reducers
function counterReducer(state={value:0}, action){
switch(action.type){
case 'getCounterStart':
return {
...state,
isFetching: true, //used to indicate that the ajax request is running
error: undefined //used to clear existing error message
}
case 'getCounterDone':
return {
...state,
isFetching: false, //used to indicate that the ajax request is running
error: undefined, //used to clear existing error message
value: action.counter.theValue
}
case 'getCounterFailed':
return {
...state,
isFetching: false, //used to indicate that the ajax request is running
error: action.error //used to show the error message
}
...
}
}
//the ajax calling as an "action"
function getCounterAction(){
const actionAsFunction = function(dispatch){
dispatch({type:'getCounterStart'}); //dispatch an "loading started" action first
return axios.get("http://localhost:12180/fo/rest/counter/get")
.then(function(response){
console.log(response.data); //success
dispatch({type:'getCounterDone', counter: response.data});
})
.catch(function(error){
if(error.response){ //failure with http response, such as a http 500 response
dispatch({type:'getCounterFailed', error: error.response.error_description_for_user})
}else{ //failure without http response
dispatch({type:'getCounterFailed', error: "Unknown Error"})
}
});
}
return actionAsFunction; //so the action itself is a function
}
//the component
class CounterComponent extends Component {
constructor(props){
super(props);
}
render(){
const errorStyle = {color:'red'};
return (
<div>
<h2>Counter</h2>
{/* Loading indicator */}
{this.props.counter.isFetching &&
<div>Loading...</div>
}
{/* Show error */}
{this.props.counter.error &&
<div style={errorStyle}>{this.props.counter.error}</div>
}
{/* Show values */}
<h3>{this.props.counter.value}</h3>
<div>
<button onClick={ e => this.props.onChange(1)}>+</button>
<button onClick={ e => this.props.onChange(-1)}>-</button>
</div>
</div>
);
}
}
class CounterComponentContainer extends Component {
componentDidMount(){
this.props.dispatch(getCounterAction()); //fetch the value when component is first rendered
}
render(){
return (
<CounterComponent counter={this.props.counter} onChange={this.props.onChange}/>
);
}
}
CounterComponentContainer = connect(
mapStateToProps,
mapStateToDispatch
) (CounterComponentContainer);
export default CounterComponentContainer;