I'm having issues with using useReducer with input. I'm trying to use controlled input here but I keep getting errors;
Controlled input is uncontrolled
import React, {useReducer, useEffect} from "react";
import axios from "axios";
const initialState = {
post: {},
user: ""
}
const reducer = (state, action) => {
switch(action.type){
case "Fetch_data":
return {
post: action.payload
}
case "On_change":
return {
user: action.payload
}
case "Fetch_error":
return {
post: {}
}
default:
return state
}
}
const ReducerFetchdata = () => {
const [info, dispatch] = useReducer(reducer, initialState)
useEffect(()=>{
axios
.get(`https://jsonplaceholder.typicode.com/posts/${info.user}`)
.then (res => {
console.log(res)
dispatch({type: "Fetch_data", payload: res.data})
})
.catch(err => {
console.log(err)
dispatch({type: "Fetch_error"})
})
}, [info.user])
const handleChange = (event) =>{
dispatch({type: "On_change", payload: event.target.value})
}
return(
<div>
<input type="text" onChange={handleChange} value={info.user}/>
<p>{info.post.title}</p>
</div>
)
}
export default ReducerFetchdata
You need to spread whole state and then update the necessary key when returning from the reducer. In this case onChange called On_change whose action did not return post from reducer state and thus causing error.
Refer : Sandbox for the fix
Just a guess: you are removing your user key after you fetch your post, so info.user becomes undefined.
Your reducer should be:
switch(action.type) {
case 'Fetch_data':
return {
...state,
post: action.payload
}
...
}
Always return
{
...state
[someKey]: someValue
}
in your On_Change and Fetch_error as well.
Related
I'm trying to fetch 'all posts' using Redux. I should be getting an empty array but instead, I'm getting undefined. Here's my reducer:
export default (posts = [], action) => {
switch ((action.type)) {
case "FETCH_ALL":
return action.payload;
case "CREATE":
return posts;
default:
return posts;
}
};
Action
export const getPosts = () => async (dispatch) => {
try {
const { data } = await api.fetchPosts();
dispatch({ type: "FETCH_ALL", payload: data });
} catch (error) {
console.log(error.message)
}
};
Posts.js component
import { useSelector } from "react-redux";
import Post from "./Post/Post";
import useStyles from "./styles";
const Posts = () => {
const posts = useSelector((state)=>state.posts)
console.log(posts)
const classes = useStyles();
return (
<>
<h1>Posts</h1>
<Post />
</>
);
};
export default Posts;
According to your reducer, your entire state is a posts array instead of a state object like { posts: [ ] }. So in your selector, you can simply return the state as it is in the Posts component.
const posts = useSelector((state)=>state);
I believe that you need to change a line in your reducer file. You need to assign the action.payload to the posts and then you can access it
export default (posts = [], action) => {
switch ((action.type)) {
case "FETCH_ALL":
return {posts: action.payload};
case "CREATE":
return posts;
default:
return posts;
}
};
Firstly, switch ((action.type)) should be switch (action.type)
Then, you should check the data from api whether it's returned
correctly or not
Finally, check your redux state object and your selector of posts
I am trying to implement an authentication system into my project and have been following this tutorial https://youtu.be/LKlO8vLvUao .
The system works however I am trying to display the error message on a failed login attempt and I cannot figure out how to get the error message from the reducer to the component where I'd like to display it (preferably using a [msg,setMsg] hook)
Here is my code from the action where I capture the error response and pass it to reducer:
export const signin = (formData, history) => async (dispatch) => {
try {
const { data } = await API.signIn(formData);
dispatch({ type: AUTH, data });
history.push('/')
}
catch (error){
const data = error.response.data;
dispatch({ type: "AUTH_FAIL", data })
}
}
Here is my reducer:
import { AUTH, LOGOUT } from '../constants/actionTypes'
const authReducer = (state = { authData: null }, action) => {
switch (action.type){
case AUTH:
localStorage.setItem('profile', JSON.stringify({ ...action?.data }))
return { ...state, authData: action?.data};
case LOGOUT:
localStorage.clear();
return { ...state, authData: null };
case "AUTH_FAIL":
return { ...state, authData: action?.data};
default:
return state;
}
}
export default authReducer;
In Reducer create initiateState object
const initiateState: {
authData: null,
error: null
}
as you did in action
catch (error){
**const data = data.response.error;**
dispatch({ type: "AUTH_FAIL", data })
}
also change reducer , and send the amount of action.data to error
const authReducer = (state = initiateState, action) => {
....//your code
case "AUTH_FAIL":
return { ...state, error: action.data};
default:
return state;
}
}
using Hooks
this equal to mapStateToProps
const error= useSelector((state) => state.error)
Where you are defining the state for this reducer, break it into two parts.one being authData as it is now, the second being error. When dispatching AUTH_FAIL, what you should return in the reducer, is the error instead of the authData. Now in your component you can call the state anyway you like and use the updated state. I know of 2 ways for doing this:
1-mapStatetoProps
2-store.subscribe() and store.getState()
I’m working on a little proof of concept project, using React and Redux, and useSelector and useDispatch hooks. I’m trying to fetch some data asynchronously and I use thunks for that. I think I'm conceptually missing something. Even though my state works as expected, I can not get my data from api using useSelector.
Here is the code. My action:
import axios from "axios";
export const API_FETCH_POSTS = 'API_FETCH_POSTS';
export const fetchPosts = (postId) => { // to simulate post request
return (dispatch) => {
let baseUrl = 'https://jsonplaceholder.typicode.com/';
let postFix = 'comments?postId=';
let url = `${baseUrl}${postFix}${postId}`;
axios.get(url)
.then(response => {
const data = response.data;
console.log(JSON.stringify(data)); // work!
dispatch(fetchPostsSuccess(data));
});
}
};
const fetchPostsSuccess = posts => {
return {
type: API_FETCH_POSTS,
payload: posts
}
};
My reducer:
import {API_FETCH_POSTS} from "./apiActions";
const initialState = {
getPostsReq : {
posts: [],
}
};
const apiReducer = (state = initialState, action) => {
let getPostsReq;
switch (action.type) {
case API_FETCH_POSTS:
getPostsReq = {
posts: [...state.getPostsReq.posts]
};
return {
...state,
getPostsReq
};
default: return state;
}
};
export default apiReducer;
And rootReducer:
import {combineReducers} from 'redux';
import apiReducer from "./api/apiReducer";
export default combineReducers({
api: apiReducer
})
And store:
const initialState = {};
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(
applyMiddleware(thunk)
)
);
export default store;
I have a problem with my React component:
function PostContainer(props) {
const posts = useSelector(state => state.api.getPostsReq.posts);
const dispatch = useDispatch();
const logPosts = () => {
{/*why doesn't this line work???*/}
console.log(JSON.stringify(posts));
}
return (
<div>
<button onClick={() => {
dispatch(fetchPosts(1));
logPosts();
}}>Fetch Posts</button>
<div>
{/*why doesn't this line work???*/}
{posts.map(post => <p>{post.body}</p>)}
</div>
</div>
);
}
export default PostContainer;
I expect that after I press the button, the function fetchPosts gets dispatched and because I use thunk I shouldn’t have any problems with asynchronicity. But by some reason I can’t get my state, using useSelector() hook. I can neither render the state, nor log it in the console.
What am I missing here?
Here is the whole code if it is more convenient - https://github.com/JavavaJ/use-select-problem
Problem: Not Storing Posts
Your selector is fine, it's your reducer that's the problem! You dispatch an action which has an array of posts in the payload:
const fetchPostsSuccess = posts => {
return {
type: API_FETCH_POSTS,
payload: posts
}
};
But when you respond to this action in the reducer, you completely ignore the payload and instead just return the same posts that you already had:
const apiReducer = (state = initialState, action) => {
let getPostsReq;
switch (action.type) {
case API_FETCH_POSTS:
getPostsReq = {
posts: [...state.getPostsReq.posts]
};
return {
...state,
getPostsReq
};
default: return state;
}
};
Solution: Add Posts from Action
You can rewrite your reducer like this to append the posts using Redux immutable update patterns.
const apiReducer = (state = initialState, action) => {
switch (action.type) {
case API_FETCH_POSTS:
return {
...state,
getPostsReq: {
...state.getPostsReq,
posts: [...state.getPostsReq.posts, ...action.payload]
}
};
default:
return state;
}
};
It's a lot easier if you use Redux Toolkit! With the toolkit you can "mutate" the draft state in your reducers, so we don't need to copy everything.
const apiReducer = createReducer(initialState, {
[API_FETCH_POSTS]: (state, action) => {
// use ... to push individual items separately
state.getPostsReq.posts.push(...action.payload);
}
});
I am using useReducer hook to manage my state, but it seems like I have a problem with reading updated state in my context provider.
My context provider is responsible to fetch some remote data and update the state based on responses:
import React, { useEffect } from 'react';
import useAppState from './useAppState';
export const AppContext = React.createContext();
const AppContextProvider = props => {
const [state, dispatch] = useAppState();
const initialFunction = () => {
fetch('/some_path')
.then(res => {
dispatch({ type: 'UPDATE_STATE', res });
});
};
const otherFunction = () => {
fetch('/other_path')
.then(res => {
// why is `state.stateUpdated` here still 'false'????
dispatch({ type: 'DO_SOMETHING_ELSE', res });
});
}
};
const actions = { initialFunction, otherFunction };
useEffect(() => {
initialFunction();
setInterval(otherFunction, 30000);
}, []);
return (
<AppContext.Provider value={{ state, actions }}>
{props.children}
</AppContext.Provider>
)
};
export default AppContextProvider;
and useAppState.js is very simple as:
import { useReducer } from 'react';
const useAppState = () => {
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_STATE':
return {
...state,
stateUpdated: true,
};
case 'DO_SOMETHING_ELSE':
return {
...state,
// whatever else
};
default:
throw new Error();
}
};
const initialState = { stateUpdated: false };
return useReducer(reducer, initialState);
};
export default useAppState;
The question is, as stated in the comment above, why is state.stateUpdated in context provider's otherFunction still false and how could I access state with latest changes in the same function?
state will never change in that function
The reason state will never change in that function is that state is only updated on re-render. Therefore, if you want to access state you have two options:
useRef to see a future value of state (you'll have to modify your reducer to make this work)
const updatedState = useRef(initialState);
const reducer = (state, action) => {
let result;
// Do your switch but don't return, just modify result
updatedState.current = result;
return result;
};
return [...useReducer(reducer, initialState), updatedState];
You could reset your setInterval after every state change so that it would see the most up-to-date state. However, this means that your interval could get interrupted a lot.
const otherFunction = useCallback(() => {
fetch('/other_path')
.then(res => {
// why is `state.stateUpdated` here still 'false'????
dispatch({ type: 'DO_SOMETHING_ELSE', res });
});
}
}, [state.stateUpdated]);
useEffect(() => {
const id = setInterval(otherFunction, 30000);
return () => clearInterval(id);
}, [otherFunction]);
I am trying update state using react-redux but the state is not being updated.
new value is coming to
"if (action.type === 'SET_LOGGED_IN')"
in reducer, but not updating the isLoggedIn as true.
What was wrong?
find the code
Login.js
function handleLoginClick(username, password, e) {
e.preventDefault();
post('/createUser', { username, password })
.then(({ status }) => {
if (status === 200) {
console.log(this.props);
this.props.setLoggedIn(true);
this.props.history.push('/');
}else{
this.props.setLoggedIn(false);
}
})
.catch(error => {
.........
})
}
..................
const mapStateToProps = state => {
return {isLoggedIn : state.reducer.isLoggedIn};};
const mapDispatchToProps = dispatch => {
return {setLoggedIn : (value) => dispatch({type: 'SET_LOGGED_IN', value: value}),}};
export default compose(
withStyles(styles),
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(NewLogin);
reducer.js
import { combineReducers } from 'redux';
import { reducer as reduxFormReducer } from 'redux-form';
const initialStates = {
isLoggedIn : false
};
const reducers = combineReducers({
reducer : (state = initialStates, action) => {
//console.log(action);
if (action.type === 'SET_LOGGED_IN') {
//console.log(action);
return {
...state,
isLoggedIn: action.value
};
}
return state;
},
form: reduxFormReducer, // mounted under "form"
});
export default reducers;
-Fixed the error-
In my code, state is updated correctly. When I accessing, I had used state.isLoggedIn which is undefined. I replaced it from state.reducer.isLoggedIn.
Now everything works charm.
Thank you #UmairFarooq , #VladimirBogomolov and all who commented and gave a try to fix it.
const mapStateToProps = state => {
return {
isLoggedIn : state.reducer.isLoggedIn
};
};
Think there might be a problem with your mapStateToProps. Try accessing your isLoggedIn state like :
const mapStateToProps = state => {
return {isLoggedIn : state.isLoggedIn};};
use componentwillupdate() to controllr when to update
return false to not update
You should be calling dispatch differently:
const mapDispatchToProps = (dispatch, val) => {
return {
setLoggedIn : (val) => dispatch({ type: 'SET_LOGGED_IN', value: val }),
}
};
Try that and it should work for you.