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
Related
I have a "My Profile" form that displays the details of the user.
Api call to fetch user data is as follows.
componentDidMount() {
this.props.getUserDetails();
}
Saga file is as follows
function* fetchUserDetails() {
try {
const response = yield call(userDetailsApi);
const user = response.data.user;
// dispatch a success action to the store
yield put({ type: types.USER_DETAILS_SUCCESS, user});
} catch (error) {
// dispatch a failure action to the store with the error
yield put({ type: types.USER_DETAILS_FAILURE, error });
}
}
export function* watchUserFetchRequest() {
yield takeLatest(types.USER_DETAILS_REQUEST, fetchUserDetails);
}
Reducer is as follows
export default function reducer(state = {}, action = {}) {
switch (action.type) {
case types.USER_DETAILS_SUCCESS:
return {
...state,
user: action.user,
loading: false
};
default:
return state;
}
}
Now i need to set the user details in state so that when the form values are changed, i can call the handleChange function to update the state.
If i had used redux thunk, i could have used something like as follows
componentDidMount() {
this.props.getUserDetails().then(() => {
this.setState({ user });
});
}
so that the user state contains all details of user and if a user property changes then the state can be updated using handleChange method .
That is,
After the api call, i need is something like
state = {
email: user#company.com,
name: 'Ken'
}
How to achieve the same using redux saga?
I have a connected component which does this:
componentWillMount() {
this.props.fetchMessage();
this.props.fetchPlots();
}
The first function returns a string, the second an array of objects.
First function:
export function fetchMessage() {
return function (dispatch) {
axios.get(ROOT_URL, {
headers: {
authorization: localStorage.getItem('token')}
})
.then(response => {
dispatch({
type: FETCH_MESSAGE,
payload: response.data.message
});
});
}
}
The second function is very similar.
Types are like this:
export const FETCH_MESSAGE = 'fetch_message';
Reducers follow this format:
case FETCH_MESSAGE:
return { ...state, message: action.payload }
case FETCH_PLOTS:
return { ...state, plots: action.payload }
I want to test it with Mocha and Chai. I'm trying to pass state like this:
beforeEach(() =>
{
component = renderComponent(Component, null, {
message: 'teststring',
plots: [
{...},{...},{...}
]
});
});
at which point, I get the following error:
Error: Actions must be plain objects. Use custom middleware for async actions.
at dispatch (C:...\node_modules\redux\lib\createStore.js:165:13)
at Object.fetchMessage (C:...\node_modules\redux\lib\bindActionCreators.js:7:12)
at Dashboard.componentWillMount (C:.../src/components/component.js:9:20)
How can I simulate the action/pass the object into state without it puking on me?
It's my first experience with React, Redux and I am totally lost. The problem is my action :
import axios from 'axios';
import { FETCH_MOVIE } from '../constants/actionTypes';
const API_KEY = <API_KEY>;
const ROOT_URL = `<API_URL>`;
export function fetchMovies(pop){
const url = `${ROOT_URL}?api_key=${API_KEY}&sort_by=${pop}`;
axios.get(url)
.then(function (response) {
console.log("response is",response)
})
.catch(function (error) {
console.log(error);
});
return{
type: FETCH_MOVIE,
payload: response.data
};
}
On Console.log it seems just fine - I can see the response has the data I need. But when I am trying to send response.data to payload it returns the error - response is not defined. What am I doing wrong?
P.s. I also tried to create const result = [] and than result = [...response.data]. The error was - SyntaxError: "result" is read-only
The const error is because, result being a variable that changes over the course of the execution, you must use 'let' and not 'const'.
Now, for the fix, response is not defined comes from the last return. A good approach would be to, instead of returning the action on this function fetchMovies, you should dispatch a new action, e.g dispatch(fetchMoviesSuccess(payload)) instead of "console.log("response is",response)", which will dispatch an action that will trigger the reducer, and , in turn, update the state of the app.
You are performing async request using axios. You should dispatch your action using redux-thunk. Installation is easy, read more about thunk here.
Then your action should look like this:
export function fetchMovies(pop) {
return dispatch => {
const url = `${ROOT_URL}?api_key=${API_KEY}&sort_by=${pop}`;
axios.get(url)
.then(function (response) {
console.log("response is",response);
dispatch({
type: FETCH_MOVIE,
payload: response.data
});
})
.catch(function (error) {
console.log(error);
// You can dispatch here error
// Example
dispatch({
type: FETCH_MOVIE_FAILED,
payload: error
});
});
}
}
The issue with your code is that by the time you return, response is still undefined because this code run synchronously till the return statement.
As you can see response is defined in console.log("response is",response)
So this is where you need to do your actual magic return but in another way.
You can use redux-thunk to do these thing because this is redux async. but as I feel you are a beginner from the code I have seen, Just use the simpler way and read redux-thunk or redux-promise. if you feel your project needs this then go one.
//try to make the caller pass this.props.dispatch as param
export function fetchMovies(dispatch, pop){
const url = `${ROOT_URL}?api_key=${API_KEY}&sort_by=${pop}`;
axios.get(url)
.then(function (response) {
// only here is response define so use dispatch to triger another action (fetched data with response)
dispatch({
type: FETCH_MOVIE,
payload: response.data
})
})
.catch(function (error) {
//if you had a loader state, you call also toggle that here with erro status
console.log(error);
});
}
//now on the caller (onClick for instance) do this instead
fetchMovies(this.props.dispatch, pop)
As you can see from #loelsonk answer down. if you use redux-thunk then you won't need to pass dispatch from the caller redux-thunk for you. But also notice how you would return and anonymous arrow function which accept dispatch as a parameter.
You can use redux promise middleware. I have used this in my new project. It is very simple and keeps our code and state manageable.
For every async action dispatch, it dispatches
$action_type_PENDING immediately after our action dispatch , $action_type_FULFILLED if api call success, $action_type_REJECTED if api call failure
See documentation- https://github.com/pburtchaell/redux-promise-middleware
Example from my project-
your action is
export function getQuestions() {
return {
type: types.GET_QUESTIONS,
payload: axios.get('http://localhost:3001/questions')
};
}
reducer is
const initialState = {
isLoading: false,
questions: []
};
const questions = (state = initialState.questions, action) => {
switch(action.type) {
case types.GET_QUESTIONS_FULFILLED:
return [...action.payload.data];
default: return state;
}
};
For displaying loader while api call we can use following reducer
const isLoading = (state = initialState.isLoading, action) => {
switch(action.type) {
case (action.type.match(/_PENDING/) || {}).input:
return true;
case (action.type.match(/_FULFILLED/) || {}).input:
return false;
default: return state;
}
};
Comment me if you need any more details on above stuff.
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.
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.