I'm working on a React application to render content of a WordPress website using the WordPress Rest API, Redux and Thunk.
The WordPress API returns posts without detailed information about the category (name, slug, etc). All I'm getting is the id. I'm currently calling an additional action/function to get the detailed category information (output). Below an example of how I'm currently fetching my posts.
// Actions.js
import axios from 'axios'
export const fetchPosts = (page = 1) => {
return {
type: "FETCH_POSTS",
payload: axios.get(`${REST_URL}/wp/v2/posts?per_page=14&page=${page}`)
}
}
|
// PostsReducer.js
const initialState = {
posts: [],
fetching: false,
fetched: false,
error: null
}
export default function reducer(state=initialState, action) {
switch (action.type) {
case "FETCH_POSTS": {
return {
...state,
fetching: true
}
}
case "FETCH_POSTS_REJECTED": {
return {
...state,
fetching: false,
error: action.payload
}
}
case "FETCH_POSTS_FULFILLED": {
return {
...state,
fetching: false,
fetched: true,
posts: action.payload
}
}
}
return state
}
And this is how I'm fetching category information:
export const fetchCategory = (id) => {
return {
type: "FETCH_CATEGORY",
payload: axios.get(`${REST_URL}/wp/v2/categories/${id}`)
}
}
Is there a way to combine my fetchPosts() action with the fetchCategory() action, so it populates the post.categories, returned from fetchPosts() with the more detailed fetchCategory() information?
If you're referring for ajax calls chaining you can use this example to understand how thunk can work for you:
function loadSomeThings() {
return dispatch => {
fetchFirstThingAsync.then(data => { // first API call
dispatch({ type: 'FIRST_THING_SUCESS', data }); // you can dispatch this action if you want to let reducers take care of the first API call
return fetchSecondThingAsync(data), // another API call with the data received from the first call that returns a promise
})
.then(data => {
dispatch({ type: 'SECOND_THING_SUCESS', data }); // the reducers will handle this one as its the object they are waiting for
});
};
}
Basically when we call loadSomeThings we dispatch an new action as a function (fetchFirstThingAsync) as our first ajax call, redux-thunk will catch that before any reducer does as function are not the plain object that reducers can handle, thunk will invoke this function with dispatcher as an argument (along getState and some more args), we wait it out with .then and then we can dispatch a plain object that reducers can handle + returning another promise (fetchSecondThingAsync) that's your second ajax call, we wait it out with .then and again dispatching a plain object that reducers can handle.
Related
I am creating an async action in Redux with Redux-Thunk and while the action works and returns the data, when it is added to the state it is put inside an object with the same name as the reducer, like so...
The posts key should just have the array of items but it is instead an object with a posts key. What could be causing this issue? Here is the code for the async action...
export function getApiData() {
return dispatch => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json())
.then(json => dispatch(
{
type: ActionTypes.GET_API_DATA,
payload: json
}
))
}
}
Here is the code for the reducer...
function posts(state = [], action) {
switch (action.type) {
case ActionTypes.GET_API_DATA:
return {
...state,
posts: action.payload,
}
default:
return state
}
}
The other keys like user and prizes (which are not async actions) work perfectly fine and the new data is added as expected to them. What is leading to this behavior when it comes to the posts key and the async action?
Thanks in advance.
Thge error is in how you process the action in posts reducer. You need to return the array instead of an object
function posts(state = [], action) {
switch (action.type) {
case ActionTypes.GET_API_DATA:
return [
...state,
...action.payload,
] // or return action.payload if you don't want to merge data
default:
return state
}
}
Are you sure that the response is in the correct format? Could you try attach a debugger or a console.log to see the data? Or try send to dispatch the json.data.
I'm trying to build out a feature in my React application showing num of comments for a specific post. Since I don't have this information from backend ill try to make a .lengthon the returned state.
However, it seems like I have built out the reducer in a faulty way but I'm not sure whats wrong with it. Right now I'm just receiving undefined.
Built up as following
Action
export function getPostComments(id) {
const request = API.fetchPostComments(id)
return (dispatch) => {
request.then(({data}) => {
dispatch({type: COMMENTS_GET_POSTCOMMENTS, payload: data})
});
};
}
Reducer
export default function(state = {}, action) {
switch (action.type){
case COMMENTS_GET_POSTCOMMENTS:
return {...state, ...action.payload}
Component
componentWillMount() {
this.props.getPostComments(this.props.id);
}
....
<span>{`comments ${this.props.comments.length}`}</span>
....
function mapStateToProps(state) {
return {
comments: state.comments,
}
}
export default connect(mapStateToProps, {postPostVote, getPostComments})(PostComponent);
EDIT
I am retrieving information from the server if I change my reducer to be return action.payloadI will first receive a comment number of 2 but then this gets wiped replacing it with a 0 since the last post in the list doesn't have any comments. So I'm overwriting here? And that most be wrong aswell
Repo : https://github.com/petterostergren/readable
Thanks for now!
export function getAllCategories() {
return (dispatch) => {
API.fetchAllCategories()
.then(data => {
dispatch({type: CATEGORIES_GET_ALL_CATEGORIES, payload: data})
});
};
}
The call to your API fetchAllCategories is asynchronous, what you were doing before was that you were calling your API but not waiting for it's response. That is why you were getting undefined passed in payload.
So what you needed to do was Chain that fetch call with the another promise.
I am using redux-thunk in my app, and this is how I am using it. See the code below.
export function loadPayments () {
return dispatch => PaymentsAPI.getCustomerPaymentMethods()
.then((paymentMethods) => {
dispatch({
type: actionTypes.LOAD_PAYMENTS_SUCCESS,
payments: paymentMethods
})
})
.catch((error) => {
console.log('Error', error);
})
}
For API Calls I am using Fetch & Axios. You can use any you want. Both are good.
To update your reducer, so that it adds the previous value do the following
case actionTypes.LOAD_SAVED_CARDS_SUCCESS: {
return {
...state,
payments: [ ...state.payments, ...action.payments],
totalpayments: state.payments.length + action.payments.length
};
}
What the reducers will do here is that, it will append all your suppose payments methods i,e previous methods + new methods along with the count update as well.
I am using React and Redux to create a login system with Google Firebase. I am trying to understand how to use thunks. I am calling my action createUser from my React component however, I'm not able to handle the callback successfully.
Here is the component function I am calling the action from:
registerUser() {
let email = this.state.user.email;
let pw= this.state.user.password;
this.props.actions.createUser(email, pw)
.then((user) => {
debugger; // The async request is successful but execution doesn't pause here
})
.catch((error) => {
debugger; // Instead I receive an error here that says, "Uncaught (in promise) RangeError: Maximum call stack size exceeded"
});
}
Here are the actions:
export function createUserSuccess(user) {
debugger;
return { type: types.CREATE_USER_SUCCESS, payload: { registeredUser: user, registerMsg: 'Successful Registration!' }};
}
export function createUserError(error) {
return { type: types.CREATE_USER_ERROR, payload: { registeredUser: {}, registerMsg: error.message }};
}
export function createUser(email, pw) { // async thunk
debugger;
return (dispatch) => {
debugger;
return firebase.auth().createUserWithEmailAndPassword(email, pw)
.then((user) => {dispatch(createUserSuccess(user))}) // todo: figure out why this won't resolve
.catch(error => dispatch(createUserError(error)));
}
}
And my Reducer:
import * as types from '../actions/actionTypes';
import initialState from './initialState';
export default function registerReducer(state = initialState.registeredUser, action) {
debugger;
switch (action.type) {
case types.CREATE_USER_SUCCESS:
return [
...state, // es6 spread operator - explodes all values in array
Object.assign({}, action.payload)
];
case types.CREATE_USER_ERROR:
return [
...state,
Object.assign({}, action.payload)
];
default:
return state;
}
}
I know the actual request to Google firebase is OK because the createUserSuccess action creator gets fired. Why isn't execution stopping at the appropriate place in my React Component?
You can check here this implementation
The Service when we read the user auth and set the value to Redux
https://github.com/x-team/unleash/blob/develop/app/services/authService.js
The reducer when set the user state to the redux state object
https://github.com/x-team/unleash/blob/develop/app/reducers/userReducer.js
The action creators
https://github.com/x-team/unleash/blob/develop/app/actions/UserActions.js
The most important part is the authService, let me know any question
I have the following middleware that I use to call similar async calls:
import { callApi } from '../utils/Api';
import generateUUID from '../utils/UUID';
import { assign } from 'lodash';
export const CALL_API = Symbol('Call API');
export default store => next => action => {
const callAsync = action[CALL_API];
if(typeof callAsync === 'undefined') {
return next(action);
}
const { endpoint, types, data, authentication, method, authenticated } = callAsync;
if (!types.REQUEST || !types.SUCCESS || !types.FAILURE) {
throw new Error('types must be an object with REQUEST, SUCCESS and FAILURE');
}
function actionWith(data) {
const finalAction = assign({}, action, data);
delete finalAction[CALL_API];
return finalAction;
}
next(actionWith({ type: types.REQUEST }));
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I am then making the following calls in componentWillMount of a component:
componentWillMount() {
this.props.fetchResults();
this.props.fetchTeams();
}
fetchTeams for example will dispatch an action that is handled by the middleware, that looks like this:
export function fetchTeams() {
return (dispatch, getState) => {
return dispatch({
type: 'CALL_API',
[CALL_API]: {
types: TEAMS,
endpoint: '/admin/teams',
method: 'GET',
authenticated: true
}
});
};
}
Both the success actions are dispatched and the new state is returned from the reducer. Both reducers look the same and below is the Teams reducer:
export const initialState = Map({
isFetching: false,
teams: List()
});
export default createReducer(initialState, {
[ActionTypes.TEAMS.REQUEST]: (state, action) => {
return state.merge({isFetching: true});
},
[ActionTypes.TEAMS.SUCCESS]: (state, action) => {
return state.merge({
isFetching: false,
teams: action.payload.response
});
},
[ActionTypes.TEAMS.FAILURE]: (state, action) => {
return state.merge({isFetching: false});
}
});
The component then renders another component that dispatches another action:
render() {
<div>
<Autocomplete items={teams}/>
</div>
}
Autocomplete then dispatches an action in its componentWillMount:
class Autocomplete extends Component{
componentWillMount() {
this.props.dispatch(actions.init({ props: this.exportProps() }));
}
An error happens in the autocomplete reducer that is invoked after the SUCCESS reducers have been invoked for fetchTeams and fetchResults from the original calls in componentWillUpdate of the parent component but for some reason the catch handler in the middleware from the first code snippet is invoked:
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I do not understand why the catch handler is being invoked as I would have thought the promise has resolved at this point.
Am not completely sure, it's hard to debug by reading code. The obvious answer is because it's all happening within the same stacktrace of the call to next(actionWith({ type: types.SUCCESS, payload: { response } })).
So in this case:
Middleware: Dispatch fetchTeam success inside Promise.then
Redux update props
React: render new props
React: componentWillMount
React: Dispatch new action
If an error occurs at any point, it will bubble up to the Promise.then, which then makes it execute the Promise.catch callback.
Try calling the autocomplete fetch inside a setTimeout to let current stacktrace finish and run the fetch in the next "event loop".
setTimeout(
() => this.props.dispatch(actions.init({ props: this.exportProps() }))
);
If this works, then its' the fact that the event loop hasn't finished processing when the error occurs and from the middleware success dispatch all the way to the autocomplete rendered are function calls after function calls.
NOTE: You should consider using redux-loop, or redux-saga for asynchronous tasks, if you want to keep using your custom middleware maybe you can get some inspiration from the libraries on how to make your api request async from the initial dispatch.
I am trying to understand Redux and having some difficulty.
I understand the concept of combineReducer, ie ....
var reducer = combineReducers({
user: userReducer,
products: productsReducer
})
But what if I have thousands of products, only available on the products page. I do not understand why I need to load them at root; to me this will slow the initial start up of the app for something that will not be needed unless the user goes to the products page.
Is this just the way it is with redux?
In Redux apps, you always build your entire state at the start. With Redux you have one store and one state - everything should trickle down from that one state to props on your components. However, that does not mean you actually need to load all the data into the state at launch, only that the structure needs to be there. This is why you should set up an initial state object for each reducer.
Let's say you have thousands of product records that you load from the database. In your products reducer you could do something like this:
const initialState = {
data: []
};
//use ES6 default parameters
function productsReducer (state = initialState, action) {
switch (action.type) {
case 'GET_PRODUCTS':
//return data from action
return {
data: action.result
};
default:
return state;
}
}
This means that when you start your app, if you use the full reducer you declared in your post, your application state will look like this:
{
user: {},
products: {
data: []
}
}
products.data will be an empty array until you fire an action that actually requires you to load the products data (i.e. you go to the Products page in your app or something). It's true that the products data will remain in your state if you then go elsewhere in your app, but this is a great thing - the next time you render the Products page you will already have the data at your disposal without having to do a database lookup.
In our app, we made an API for the products and it has limit of 15 per page. So our reducer goes like this.
collection: {
"total": 0,
"per_page": 0,
"current_page": 0,
"last_page": 0,
"from": 0,
"to": 0,
data: []
},
isFetching: false,
isFetchingError: false
on the first load we fetched limited amount of products, then we made a pagination out of it.. using selectors in redux https://github.com/rackt/reselect
Loading a thousands of data will get your app very slow.
const paginated = (state = initialState, action) => {
switch (action.type) {
case FETCH_PAGINATED_PRODUCTS:
return {
...state,
isFetching: true,
isFetchingError: false
};
case FETCH_PAGINATED_PRODUCTS_SUCCESS:
return {
...state,
collection: action.payload,
isFetching: false
};
case FETCH_PAGINATED_PRODUCTS_ERROR:
return {
...state,
isFetching: false,
isFetchingError: true
};
default:
return state
we have used axios for request:
https://github.com/mzabriskie/axios
Here's how we implement axios in redux-async
export function getAll(page = 1) {
return (dispatch, getState) => {
const state = getState();
const { filters } = state.products.paginated;
if ( state.products.paginated.isFetching ) {
return;
}
dispatch({ type: FETCH_PAGINATED_PRODUCTS });
return axios
.get(`products?page=${page}&limit=16&filters=${JSON.stringify(filters)}`)
.then((res) => dispatch({
type: FETCH_PAGINATED_PRODUCTS_SUCCESS,
payload: res.data
}))
.catch((res) => dispatch({
type: FETCH_PAGINATED_PRODUCTS_ERROR,
/*payload: res.data.error,*/
error: true
}));
}
}
export function get(id) {
return (dispatch, getState) => {
const state = getState();
if ( state.products.resource.isFetching ) {
return;
}
dispatch({ type: FETCH_PRODUCT });
return axios
.get(`products/${id}`)
.then((res) => dispatch({
type: FETCH_PRODUCT_SUCCESS,
payload: res.data.data
}))
.catch((res) => dispatch({
type: FETCH_PRODUCT_ERROR,
/*payload: new Error(res.data.error),*/
error: true
}));
}