Redux async action triggered after request finished. Why? - react-redux

I have problem with my async action. I would like to set 'loading' state to true when action fetchPosts() is called and 'loading' state to false when action fetchPostsSuccess() or fetchPostsFailiure().
With my current code it works almost fine except 'loading' state change when fetchPosts() receive response from server and I would like to change this state at the beginning of request.
Here is simple code which shows my steps.
I'm using axios and redux-promise (https://github.com/acdlite/redux-promise).
// actions
export function fetchPosts() {
const request = axios.get(`${API_URL}/posts/`);
return {
type: 'FETCH_POSTS',
payload: request,
};
}
export function fetchPostsSuccess(posts) {
return {
type: 'FETCH_POSTS_SUCCESS',
payload: posts,
};
}
export function fetchPostsFailure(error) {
return {
type: 'FETCH_POSTS_FAILURE',
payload: error,
};
}
// reducer
const INITIAL_STATE = {
posts: [],
loading: false,
error: null,
}
const postsReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'FETCH_POSTS':
return { ...state, loading: true, error: null };
case 'FETCH_POSTS_SUCCESS':
return { ...state, posts: action.payload, loading: false };
case 'FETCH_POSTS_FAILURE':
return { ...state, posts: [], loading: false, error: action.payload };
default:
return state;
}
}
const rootReducer = combineReducers({
postsList: postsReducer,
});
// store
function configureStore(initialState) {
return createStore(
rootReducer,
applyMiddleware(
promise,
),
);
}
const store = configureStore();
// simple Posts app
class Posts extends Component {
componentWillMount() {
this.props.fetchPosts();
}
render() {
const { posts, loading } = this.props.postsList;
return (
<div>
{loading && <p>Loading...</p>}
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
}
const mapStateToProps = state => ({
postsList: state.postsList,
});
const mapDispatchToProps = dispatch => ({
fetchPosts: (params = {}) => {
dispatch(fetchPosts())
.then((response) => {
if (!response.error) {
dispatch(fetchPostsSuccess(response.payload.data));
} else {
dispatch(fetchPostsFailure(response.payload.data));
}
});
},
});
const PostsContainer = connect(mapStateToProps, mapDispatchToProps)(Posts);
// main
ReactDOM.render((
<Provider store={store}>
<Router history={browserHistory}>
<Route path="posts" component={PostsContainer} />
</Router>
</Provider>
), document.getElementById('appRoot'));
Can someone guide me what I'm doing wrong ?

It's turned out the problem is with 'redux-promise' package. This async middleware has no such thing like 'pending' state of promise (called 'optimistic update') .
It changes the state only when promise has been resolved or rejected.
I should use different middleware which allow for 'optimistic updates'

Your problem ís with redux-promise. You should use redux-thunk instead that allows you to return a function and dispatch multiple times. Have a look at it ;)!

Related

Redux toolkit: updating state with entityAdapter vs custom callback

In have an async request to log a user in:
export const loginUser = createAsyncThunk('users/login', async userInputs => {
try {
const { data } = await axios.post(
'url',
userInputs
);
Cookies.set('user', JSON.stringify(data));
return data;
} catch (error) {
return error.response.data;
}
});
In my store slice I have:
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers(builder) {
// builder.addCase(loginUser.fulfilled, usersAdapter.addOne); // <-- updates state with a delay?
builder.addCase(loginUser.fulfilled, (state, action) => {
state.user = action.payload;
});
},
});
When a user clicks a Log In button the thunk is dispatched. When it returns, the user is redirected to the Home page, which renders conditionally:
useSelector(state => state.user) ? <Home /> : null
With the custom callback this works. However, if I switch to using createEntityAdapter with addOne in the slice:
builder.addCase(loginUser.fulfilled, usersAdapter.addOne);
The useSelector call returns null until I reload the page. Why?
Edit:
The initialState looks like this:
const initialState = {
user: Cookies.get('user') ? JSON.parse(Cookies.get('user')) : null,
};
And the loginHandler:
const loginHandler = async () => {
const data = await dispatch(
loginUser({
email,
password,
})
).unwrap();
navigate('/');
};

[updated]Intergrating NextJS and Redux State Management

this my updated version of intergrating redux and NextJS. Just to elobarate what I have done so far...
STEP 1. I've created a store.js file to set up my global store in reference to github's explanation from nextJS developers.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { createWrapper, HYDRATE } from 'next-redux-wrapper';
import thunkMiddleware from 'redux-thunk';
import { customerListReducer } from './customerReducers';
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension');
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
const combinedReducer = combineReducers({
customerList: customerListReducer,
});
const reducer = (state, action) => {
console.log('Just Displaying the Store', state);
if (action.type === HYDRATE) {
const nextState = {
...state, // use previous state
...action.payload, // apply delta from hydration
};
if (state.count) nextState.count = state.count; // preserve count value on client side navigation
return nextState;
} else {
return combinedReducer(state, action);
}
};
// create a makeStore function
const store = () =>
createStore(
reducer,
bindMiddleware([thunkMiddleware])
);
// export an assembled wrapper
export const wrapper = createWrapper(store);
STEP 2: Imported the wrapper above in my _app file to make the wrapper available across all pages in my application
import Nav from '../components/Nav';
import {wrapper} from '../reducers/store';
function MyApp({ Component, pageProps }) {
return (
<>
<Nav />
<Component {...pageProps} />
</>
);
}
export default wrapper.withRedux(MyApp);
STEP 3: CONFIGURATIONS
A) My Action that calls external API
import axios from 'axios';
import {
CUSTOMER_LIST_REQUEST,
CUSTOMER_LIST_SUCCESS,
CUSTOMER_LIST_FAIL,
} from '../constants/customerConstants';
export const listCustomers = () => async (dispatch) => {
try {
dispatch({
type: CUSTOMER_LIST_REQUEST,
});
const { data } = await axios.get(
'https://byronochara.tech/gassystem/api/v1/customers'
);
const result = data.results;
dispatch({
type: CUSTOMER_LIST_SUCCESS,
payload: result,
});
} catch (error) {
dispatch({
type: CUSTOMER_LIST_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
B)My Action Reducer
import {
CUSTOMER_LIST_REQUEST,
CUSTOMER_LIST_SUCCESS,
CUSTOMER_LIST_FAIL,
} from '../constants/customerConstants';
import { HYDRATE } from 'next-redux-wrapper';
export const customerListReducer = (state = { customers: [] }, action) => {
switch (action.type) {
case HYDRATE:
return { loading: true, customers: [] };
case CUSTOMER_LIST_REQUEST:
return { loading: true, customers: [] };
case CUSTOMER_LIST_SUCCESS:
return {
loading: false,
customers: action.payload,
};
case CUSTOMER_LIST_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
C)The finally bringing it all together in my index.js page to display the results:
import React, { useEffect } from 'react';
import Head from 'next/head';
import { useSelector} from 'react-redux';
import { listCustomers } from './../actions/customerActions';
import { wrapper } from '../reducers/store';
import styles from '../styles/Home.module.css';
const Home = () => {
//Select the loaded customers' list from central state
const customerList = useSelector((state) => {
console.log(state);
return state.customerList;
});
const { loading, error, customers } = customerList;
//displaying the customers data from the external API
console.log('Fetched Customers Data', customers);
return (
<div className={styles.container}>
<Head>
<title>Home | Next</title>
</Head>
<h1>Welcome to Home Page</h1>
{/* {loading && <h6>Loading...</h6>} */}
{/* {error && <h6>Error Occured...</h6>} */}
{/* {customers.map((customer) => (
<h3>{customer.customerName}</h3>
))} */}
{/* <ArticleList customers={customers} /> */}
</div>
);
};
// getStaticProp at build time
// getServerSideProp at every request slower
// getStaticPath to dynamically generate paths based on the data we are fetching
export const getStaticProps = wrapper.getServerSideProps(async ({ store }) => {
// console.log('STORE', store);
store.dispatch(listCustomers());
});
export default Home;
COMMENT ON THE PROBLEM I'M FACING FROM THE ABOVE CODE: once everything has been set up if you follow the code above, the code seems to run well the store is successfully created when I log the result on the console ``{ customerList: { loading: true, customers: [] } }. But then I guess this is the result from the HYDRATE action type since it will always be dispatch since am using getStaticProps``` that creates a new store instance in the server.
MAIN QUIZ: My challenge is how do I bypass the HYDRATED action and reconcile the server side state with the client side store and persist it and at least to finally be able to view the list from the external API. Thanks in advance. :)
I totally recommend you to use reduxjs/toolkit. It's very simple , less code, no wrappers, clean. And no matter your project on nextjs or created via CRA. Also you dont need to configure redux-thunk and redux-devtools cause they are enabled by default. Read documentation for more information ( how to persist state without any npm package and so on )
Here is a little example.
store.js
import { combineReducers, configureStore } from "#reduxjs/toolkit";
import userSlice from './user.slice.js';
//reducers
const rootReducer = combineReducers({
user: userSlice
});
const store = configureStore({
reducer: rootReducer,
});
export default store;
Wrap with Provider (in your case _app.js)
<Provider store={store}>
<Component {...pageProps} />
</Provider>
user.slice.js ( action + reducer )
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
const initialState = {
id: '',
email: '',
roles: []
};
// export async action
export const signIn = createAsyncThunk('user/signIn', async (data) => {
try {
const payload = await api.auth.signin(data).then((res) => res.data);
// do some stuff if you want
return payload ;
} catch (err) {
console.log(err.response);
}
});
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
removeUser(state, payload) {
//cant be an async method
return initialState;
},
extraReducers: (builder) => {
builder.addCase(signIn.fulfilled, (state, { payload }) => {
// payload from the async method above (asyncThunk)
return payload;
});
},
},
});
// export actions
export const { removeUser } = userSlice.actions;
// export reducer
export default userSlice.reducer;
Thats it. Last step to call actions from any component e.g.
import { useDispatch, useSelector } from 'react-redux';
import { signIn, removeUser } from '../actions/userSlice';
// in function component
// call hooks
const dispatch = useDispatch();
// read the store
const { user } = useSelector((state) => state);
// dispatch any action , example below
dispatch(signIn(userCredentials));
// or
dispatch(removeUser());
I has an Issue with setting Redux with NextJS and this is my final answer after some insight from mirik999 too.
A. my store.
import { configureStore } from '#reduxjs/toolkit';
//importing the slice file with sliced reducers
import customerListReducer from '../slices/customerSlice';
// const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware));
const store = configureStore({
reducer: {
customerList: customerListReducer,
},
});
export default store;
B. The store is provided in my app component
function MyApp({ Component, pageProps }) {
return (
<Provider store={store}>
<Nav />
<Component {...pageProps} />
</Provider>
);
}
export default MyApp;
C. The Slice file that automatically creates action creators and the reducer
import { createSlice } from '#reduxjs/toolkit';
//creating and action that calls API from a REST API backend
export const customersFetchedList = createAsyncThunk(
'customersList/customersListSuccess',
async () => {
try {
const { data } = await axios.get(
'https://example.com/api/your/endpoint'
);
const result = data.results;
//the payload
const payload = result;
return payload;
} catch (error) {
console.log(error.response);
const payload =
error.response && error.response.data.message
? error.response.data.message
: error.message;
return payload;
}
}
);
const initialState = {
loading: true,
customers: [],
error: false,
};
const customerListSlice = createSlice({
name: 'customersList',
initialState,
reducers: {
//reducer functions we've provided
customersRequest(state, action) {
if (state.loading == true) {
return state;
}
},
},
extraReducers: (builder) => {
initialState,
builder.addCase(customersFetchedList.fulfilled, (state, action) => {
state.loading = false;
state.customers = action.payload;
state.error = false;
return state;
});
},
});
export const {
customersRequest,
customersLoadingError,
} = customerListSlice.actions;
export default customerListSlice.reducer;
D. Then finally fired this action above in my component using the useEffect()
import React, { useEffect } from 'react';
import Head from 'next/head';
const Home = () => {
//method to fire the action
const dispatch = useDispatch();
//Select the loaded customers' list from central state
const customerList = useSelector((state) => state);
// const { loading, error, customers } = customerList;
useEffect(() => {
dispatch(listCustomers());
}, []);
return (
<div className={styles.container}>
<Head>
<title>Home | Next</title>
</Head>
<h1>Welcome to Home Page</h1>
{loading && <h6>Loading...</h6>}
{error && <h6>Error Occured...</h6>}
{customers.map((customer) => (
<h3>{customer.customerName}</h3>
))}
</div>
);
};
Thanks so much for your contribution. :)

Proper way to clear asynchronous work in redux middleware

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() }));
}
if 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 componentWillMount of the parent component and the error will be handled in the Promise.catch of the callApi method that happens in the middleware.
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()
}
}))
});
};
This is because it is happening with in the same tick of the event loop. If I introduce some asynchronicity in the Autcomplete componentWIllMount function then the error is not handled in the Promise catch handler of the middleware
class Autocomplete extends Component{
componentWillMount() {
setTimeout(() => {
this.props.dispatch(actions.init({ props: this.exportProps() }));
});
}
Should I have the callApi function execute on a separate event loop tick?

How to transition routes after an ajax request in redux

I'm wondering at a high level what the correct pattern is for the following...
I have a HomeComponent, with some links to other components.
When I click on one of the links, I want to make an ajax request to get the initial state for that component.
Do I dispatch in the HomeComponent in the onClick? Or dispatch an action in the other components if there's no initialState from the server? (I'm doing a universal app, so if I was to hit one of the other components directly, the initial state would already be there, but coming from my HomeComponent, the data WON'T be there)
This is what I had so far...
class HomeComponent extends React.Component {
navigate(e) {
e.preventDefault();
// Fetch data here
actions.fetch(1234);
// When do I call this?
browserHistory.push(e.target.href);
}
render() {
const links = [
<a href="/foo/1247462" onClick={this.navigate}>Link 1</a>,
Link 2,
];
return (
<ul>
{links.map((link) => (
<li>{link}</li>
))}
</ul>
);
}
}
Sorry i can add a comment, is there a reason you're not using react-redux && redux-thunk ?
what you ask can be easily done with those : you fetch what you need in mapDispatchToProps & dispatch an action with the fetched initial state
Your reducer will catch the said dispatched action and update its state which will update the props of the react component with the help of mapStateToProps
I am writing from memory, it might not be accurate 100% :
redux file
componentNameReducer = (
state = {
history: ''
},
type = {}
) => {
switch(action.type) {
case 'HISTORY_FETCHED_SUCCESSFULLY':
return Object.assign({}, state, {
history: action.payload.history
});
default:
return state;
}
};
mapStateToProps = (state) => {
history: state.PathToWhereYouMountedThecomponentNameReducerInTheStore.history
};
mapDispatchToProps = (dispatch) => ({
fetchHistory : () => {
fetch('url/history')
.then((response) => {
if (response.status > 400) {
disptach({
type: 'HISTORY_FETCH_FAILED',
payload: {
error: response._bodyText
}
});
}
return response;
})
.then((response) => response.json())
.then((response) => {
//do checkups & validation here if you want before dispatching
dispatch({
type: 'HISTORY_FETCHED_SUCCESSFULLY',
payload: {
history: response
}
});
})
.catch((error) => console.error(error));
}
});
module.exports = {
mapStateToProps,
mapDispatchToProps,
componentNameReducer
}
On your react component you will need :
import React, {
Component
} from 'somewhere';
import { mapStateToProps, mapDispatachToProps } from 'reduxFile';
import { connect } from 'react-redux';
class HistoryComponent extends Component {
constructor(props){
super(props);
this.props.fetchHistory(); //this is provided by { connect } from react-redux
}
componentWillReceiveProps(nextProps){
browserHistory.push(nextProps.history);
}
render() {
return (
);
}
}
//proptypes here to make sure the component has the needed props
module.exports = connect(
mapStateToProps,
mapDispatachToProps
)(HistoryComponent);

How to get ajax request in redux?

I'm stupid, I still can't get ajax request in redux. I don't understand, where should I get getState in action. In component, I using connect that link action and reducer. Then I using componentDidMount that call an action in a component. How to get ajax request in redux from server? Help me to understand this disorder. I tried to understand the examples of redux, but it's has no effect. If I start a server, get warning : getDefaultProps is only used on classic React.createClass definitions. Use a static property named defaultProps instead.
Action
import $ from 'jquery';
export const GET_BOOK_SUCCESS = 'GET_BOOK_SUCCESS';
export default function getBook() {
return (dispatch, getState) => {
$.ajax({
method: "GET",
url: "/api/data",
dataType: "application/json"
}).success(function(result){
return dispatch({type: GET_BOOK_SUCCESS, result});
});
};
}
Reducer
import {GET_BOOK_SUCCESS} from '../actions/books';
const booksReducer = (state = {}, action) => {
console.log(action.type)
switch (action.type) {
case GET_BOOK_SUCCESS:
return Object.assign({}, state, {
books: action.result.books,
authors: action.result.authors
});
default:
return state;
}
};
export default booksReducer;
component
function mapStateToProps(state) {
console.log(state)
return {
books: state.books,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({getBooks: () => getBook(),}, dispatch);
}
#Radium
#connect(mapStateToProps, mapDispatchToProps)
class booksPage extends Component {
static propTypes = {
getBooks: PropTypes.func.isRequired,
};
componentDidMount() {
const { getBooks } = this.props;
getBooks();
}
render() {
const {books} = this.props;
index.js
const store = configureStore({}, routes);
ReactDOM.render((
<div>
<Provider store={ store }>
<ReduxRouter />
</Provider>
<DebugPanel top right bottom>
<DevTools
store={ store }
monitor={ LogMonitor }
visibleOnLoad />
</DebugPanel>
</div>),
document.getElementById('root')
);
configureStore
function configureStore(initialState, routes) {
const store = compose(
applyMiddleware(
promiseMiddleware,
thunk,
logger
),
reduxReactRouter({ routes, history }),
devTools()
)(createStore)(rootReducer, initialState);
if (module.hot) {
module.hot.accept('../reducers', () => {
const nextRootReducer = require('../reducers');
store.replaceReducer(nextRootReducer);
});
}
return store;
}
export default configureStore

Resources