I just learn react-native basic, and when work with redux, i have problem with useSelector , here is some of my code
Here is store component
//store.js
initState = {
loginStatus: false,
}
const LoginAction = (state = {initState}, action) => {
if (action.type == 'changeLogin') {
return { loginStatus:!state.loginStatus }
}
return state
}
const store = createStore(LoginAction, composeWithDevTools());
export default store
Here is Login Function
function LoginScreen({ navigation, props }) {
const dispatch = useDispatch()
const Login = useSelector(state => {
return state.LoginStatus
})
function getLogin() {
return Login
}
function handleLogin() {
dispatch({ type: 'changeLogin' })
}
console.log('Login ' + Login) // it return undefined
I have tried this method useSelector state returns undefined (React-Redux) but it didn't work!
Here is screenshot of what happened
But when i add that to login button, it return true, then continute to undefined
<Formik
validateOnMount
validationSchema={loginValidationSchema}
initialValues={{ email: '', password: '' }}
onSubmit={
() => {
handleLogin()
console.log('When submit ' + Login) // true then undefined
SetTimer();
}
// () => navigation.navigate('Login')
}
>
Please help , thank a lot
The casing is wrong in your selector. It should be return state.loginStatus. Also, your LoginAction is technically a reducer, not an action.
const Login = useSelector(state => {
return state.loginStatus
})
Edit: An additional issue in the reducer is the initial state has initState as the top-level key in the object, when the intent is just for it to be assigned directly:
const LoginAction = (state = initState, action) = {
// reducer code here
}
Related
I have a react application with two buttons, which on click load user name from server. The behaviour works if I click buttons one at a time and wait for response, however, if I click both, the response from API for second button writes value to state which is stale due to which the first button gets stuck in loading state. How can I resolve this to always have latest data when promise resolves?
Code sandbox demo: https://codesandbox.io/s/pensive-frost-qkm9xh?file=/src/App.js:0-1532
import "./styles.css";
import LoadingButton from "#mui/lab/LoadingButton";
import { useRef, useState } from "react";
import { Typography } from "#mui/material";
const getUsersApi = (id) => {
const users = { "12": "John", "47": "Paul", "55": "Alice" };
return new Promise((resolve) => {
setTimeout((_) => {
resolve(users[id]);
}, 1000);
});
};
export default function App() {
const [users, setUsers] = useState({});
const availableUserIds = [12, 47];
const loadUser = (userId) => {
// Mark button as loading
const updatedUsers = { ...users };
updatedUsers[userId] = {
id: userId,
name: undefined,
isLoading: true,
isFailed: false
};
setUsers(updatedUsers);
// Call API
getUsersApi(userId).then((userName) => {
// Update state with user name
const updatedUsers = { ...users };
updatedUsers[userId] = {
...updatedUsers[userId],
name: userName,
isLoading: false,
isFailed: false
};
setUsers(updatedUsers);
});
};
return (
<div className="App">
{availableUserIds.map((userId) =>
users[userId]?.name ? (
<Typography variant="h3">{users[userId].name}</Typography>
) : (
<LoadingButton
key={userId}
loading={users[userId]?.isLoading}
variant="outlined"
onClick={() => loadUser(userId)}
>
Load User {userId}
</LoadingButton>
)
)}
</div>
);
}
The problem is that useState's setter is asynchronous, so, in your loader function, when you define const updatedUsers = { ...users };, user is not necessary updated.
Luckily, useState's setter provides allows us to access to the previous state.
If you refactor your code like this, it should work:
const loadUser = (userId) => {
// Mark button as loading
const updatedUsers = { ...users };
updatedUsers[userId] = {
id: userId,
name: undefined,
isLoading: true,
isFailed: false
};
setUsers(updatedUsers);
// Call API
getUsersApi(userId).then((userName) => {
// Update state with user name
setUsers(prevUsers => {
const updatedUsers = { ...prevUsers };
updatedUsers[userId] = {
...updatedUsers[userId],
name: userName,
isLoading: false,
isFailed: false
};
return updatedUsers
});
});
};
Here a React playground with a simplified working version.
I am using
ReactJS 16.14.0
I have a React functional component that relies on data stored in context to render correctly, some of this data needs additional processing before display and some additional data needs fetching. the component is throwing the React has detected a change in the order of Hooks error, I have read the react docs on the rules of hooks as well as having a good look through SO but I can't work out why I get the error. I have shortened the code below to keep it brief.
const { enqueueSnackbar } = useSnackbar();
const [ mainContact, setMainContact ] = useState(undefined);
const [ mainAddress, setMainAddress ] = useState(undefined);
const [ thisLoading, setThisLoading ] = useState(true);
const { organisation, addresses, loading } = useProfileState();
useEffect(() => {
setThisLoading(true);
if(organisation && addresses && !loading) {
Promise.all([getMainContact(), getMainAddress()])
.then(() => {
setThisLoading(false);
})
.catch(() => {
console.log("Failed getting address/contact info");
setThisLoading(false);
})
}
}, [organisation, addresses, loading])
const getMainContact = () => {
return new Promise((resolve, reject) => {
apiService.getData(`/organisation/users/${organisation.mainContact}`)
.then(mainContact => {
setMainContact(mainContact);
return resolve();
})
.catch(error => {
enqueueSnackbar(error, { variant: 'error' });
return reject();
})
})
}
const getMainAddress = () => {
return new Promise((resolve, reject) => {
let mainAddress = addresses.find(addr => addr.id === organisation.mainAddress)
if(mainAddress !== undefined) {
setMainAddress(mainAddress);
return resolve();
} else {
enqueueSnackbar("Error getting main address ", { variant: 'error' });
return reject();
}
})
}
}
I just want to understand why I get this error and any potential solutions or what I am doing wrong etc. below is the full error. If I comment out the setThisLoading(false) in the .then() of the Promise.all() the error goes away but my page never displays any content because I use thisLoading to conditionally render a loading wheel or the content.
------------------------------------------------------
1. useContext useContext
2. useDebugValue useDebugValue
3. useContext useContext
4. useRef useRef
5. useRef useRef
6. useRef useRef
7. useMemo useMemo
8. useEffect useEffect
9. useEffect useEffect
10. useDebugValue useDebugValue
11. useContext useContext
12. useState useState
13. useState useState
14. useState useState
15. useState useState
16. useState useState
17. useState useState
18. useState useState
19. useContext useContext
20. useEffect useEffect
21. undefined useContext
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
I am just looking to understand why the setThisLoading(false) causes me to get this error.
Update
The useSnackbar() hook is provided by an external libary notistack
https://github.com/iamhosseindhv/notistack
Below is the code relating to useProfileState()
import React, { createContext, useContext, useReducer } from 'react';
const initialState = { loggedIn: false, loading: true, error: false }
const ProfileStateContext = createContext();
const ProfileDispatchContext = createContext();
const ProfileReducer = (state, action) => {
switch (action.type) {
case 'LOGGED_IN':
console.log("PROFILE CONTEXT - Logged in");
return { ...state, loggedIn: true }
case 'LOADED':
console.log("PROFILE CONTEXT - Data loaded");
return { ...state, loading: false, error: false }
case 'LOADING':
console.log("PROFILE CONTEXT - Data loading");
return { ...state, loading: true, error: false }
case 'ERROR':
console.log("PROFILE CONTEXT - Error");
return { ...state, loading: false, error: true }
case 'ADD_USER':
console.log("PROFILE CONTEXT - Adding user...");
return { ...state, user: { ...action.payload } }
case 'ADD_ORGANISATION':
console.log("PROFILE CONTEXT - Adding organisation...");
return { ...state, organisation: { ...action.payload } }
case 'ADD_ROLES':
console.log("PROFILE CONTEXT - Adding roles...");
return { ...state, roles: [...action.payload] }
case 'ADD_ORGANISATIONS':
console.log("PROFILE CONTEXT - Adding organisations...");
return { ...state, organisations: [...action.payload] }
case 'ADD_ADDRESSES':
console.log("PROFILE CONTEXT - Adding addresses...");
return { ...state, addresses: [...action.payload] }
case 'LOGOUT':
console.log("PROFILE CONTEXT - Removing context data...");
return initialState;
default:
console.error(`Unhandled action dispatched to user reducer, action type was: ${action.type}`);
return state;
}
}
const ProfileProvider = ({ children }) => {
const [state, dispatch] = useReducer(ProfileReducer, initialState)
return (
<ProfileStateContext.Provider value={state}>
<ProfileDispatchContext.Provider value={dispatch}>
{children}
</ProfileDispatchContext.Provider>
</ProfileStateContext.Provider>
);
};
const useProfileState = () => {
const context = useContext(ProfileStateContext);
if (context === undefined) {
throw new Error('useProfileState must be used within a ProfileContextProvider')
}
return context;
};
const useProfileDispatch = () => {
const context = useContext(ProfileDispatchContext);
if (context === undefined) {
throw new Error('useProfileDispatch must be used within a ProfileContextProvider')
}
return context;
};
export {
ProfileProvider,
useProfileDispatch,
useProfileState
}
Update 2
I have also tried chaining the promises and adding a dummy cleanup func as suggested, I still get the same error.
useEffect(() => {
setThisLoading(true);
if(organisation && addresses && !loading) {
getMainContact()
.then(() => {
getMainAddress()
.then(() => {
getBillingContact()
.then(() => {
getBillingAddress()
.then(() => {
setThisLoading(false);
})
})
})
})
}
return () => {};
}, [organisation, addresses, loading])
I found the problem to be in a completely different component to the one the error was indicating towards. The setThisLoading(false) was a red herring as this just allowed the problem component to render therefore giving the error. The way I found this out was via Chrome’s console, I usually work in Firefox as this is my browser of choice but this time Chrome came to the rescue as it gave more information as to where the error was originating from.
The application I am building has the concept of user roles, allowing/denying users to perform certain tasks. I wrote some functions to assist in the disabling of buttons and/or not showing content based on the role the logged in user had. This is where the problem lies.
Old RoleCheck.js
import React from 'react';
import { useProfileState } from '../../context/ProfileContext';
//0 - No permission
//1 - Read only
//2 - Read/Write
const _roleCheck = (realm, permission) => {
const { organisation, organisations, roles, loading } = useProfileState();
if(!loading) {
//Get the RoleID for the current account
const currentOrganisation = organisations.find(org => org.id === organisation.id);
//Get the Role object by RoleID
const currentRole = roles.find(role => role.id === currentOrganisation.roleId);
if(currentRole[realm] === permission) {
return true;
} else {
return false;
}
}
};
export const roleCheck = (realm, permission) => {
//Reversed boolean for button disabling
if(_roleCheck(realm, permission)) {
return false;
} else {
return true;
}
};
export const RoleCheck = ({ children, realm, permission }) => {
if(_roleCheck(realm, permission)) {
return (
<React.Fragment>
{ children }
</React.Fragment>
);
} else {
return (
<React.Fragment />
);
}
};
Usage of old RoleCheck.js
import { roleCheck } from '../Utils/RoleCheck';
...
<Button variant="outlined" disabled={roleCheck("organisation", 2)} color="primary">
edit organisation
</Button>
New RoleCheck.js
import React from 'react';
import { useProfileState } from '../../context/ProfileContext';
//0 - No permission
//1 - Read only
//2 - Read/Write
export const useRoleCheck = () => {
const { organisation, organisations, roles, loading } = useProfileState();
const _roleCheck = (realm, permission) => {
if(!loading) {
//Get the RoleID for the current account
const currentOrganisation = organisations.find(org => org.id === organisation.id);
//Get the Role object by RoleID
const currentRole = roles.find(role => role.id === currentOrganisation.roleId);
if(currentRole[realm] === permission) {
return true;
} else {
return false;
}
}
};
const RoleCheckWrapper = ({ children, realm, permission }) => {
if(_roleCheck(realm, permission)) {
return (
<React.Fragment>
{ children }
</React.Fragment>
);
} else {
return (
<React.Fragment />
);
}
};
const roleCheck = (realm, permission) => {
//Reversed boolean for button disabling
if(_roleCheck(realm, permission)) {
return false;
} else {
return true;
}
};
return {
roleCheck: roleCheck,
RoleCheckWrapper: RoleCheckWrapper
}
}
Usage of new RoleCheck.js
import { useRoleCheck } from '../Utils/RoleCheck';
...
const RequiresRoleCheck = () => {
const rc = useRoleCheck();
return (
<Button variant="outlined" disabled={rc.roleCheck("organisation", 2)} color="primary">
edit organisation
</Button>
)
}
By turning my Role Check functions into a hook I am able to call hooks inside it and I am able to call the useRoleCheck() hook at the top level of components that need to use it therefore not breaking the rules of hooks!
Summary
I have 2 states: isLogged and counter. isLogged is a bool where the user can set it to true or false, in doing so the user can access certain pages where if isLogged is set to true. The counter is just a simple int where the user can increment/decrement it. Both states are saved on sessionStorage if their values are changed; and on init, I ran a dispatch to get both values from the sessionstorage. The default value for isLogged is false, and counter is 0.
The Problem
Let's say for example we have this state values:
isLogged = true
counter = 4
If I try to call an increment action, the values will now be this:
isLogged = false
counter = 5
As you can see, it resets the isLogged to its default value. However when I look on the sessionstorage panel on Chrome, it says there: {isLogged: true, counter: 5} so it is still saving the values on the sessonstorage and everything there seems to work fine. However on the app itself, it says that isLogged is now false and the user cannot access certain pages anymore even if he didn't logout for some reason.
What's weird is if I refresh the page, the states are now:
isLogged = true
counter = 5
(of course the data have been fetched from the sessionstorage) But if I now switch the isLogged to false, it will also reset the counter to 0:
isLogged = false
counter = 0
So the problem is when I call a dispatch/action to modify a certain state, all other states get reset.
Codes
src/actions/index.js
export const increment = (num) => {
return {
type: 'INCREMENT',
payload: num
}
}
export const decrement = () => {
return {
type: 'DECREMENT'
}
}
export const get_data = () => {
return {
type: 'GET'
}
}
export const signin = () => {
return {
type: 'SIGN_IN'
}
}
export const signout = () => {
return {
type: 'SIGN_OUT'
}
}
export const getlogged = () => {
return {
type: 'GET'
}
}
src/reducers/counter.js
const counterReducer = (state = 0, action) => {
switch(action.type) {
case 'INCREMENT':
state += action.payload
saveCounter(state)
return state
case 'DECREMENT':
state -= 1
saveCounter(state)
return state
case 'GET':
state = getCounter()
return state
default:
return 0
}
}
const saveCounter = (state) => {
const data = {
counter: state
}
sessionStorage.setItem("data", JSON.stringify(data))
}
const getCounter = () => {
if (sessionStorage.getItem("data") != null) {
const data = JSON.parse(sessionStorage.getItem("data"))
return data["counter"]
}
else {
return 0
}
}
export default counterReducer
src/reducers/isLogged.js
const loggedReducer = (state = false, action) => {
switch(action.type) {
case 'SIGN_IN':
state = true
saveLogged(state)
return state
case 'SIGN_OUT':
state = false
saveLogged(state)
return state
case 'GET':
state = getLogged()
return state
default:
return false
}
}
const saveLogged = (state) => {
const data = {isLogged: state}
sessionStorage.setItem("logged", JSON.stringify(data))
}
const getLogged = () => {
if (sessionStorage.getItem("logged") != null) {
const data = JSON.parse(sessionStorage.getItem("logged"))
return data["isLogged"]
}
else {
return false
}
}
export default loggedReducer
I'm calling it on a component like this:
useEffect(() => {
getCounter()
}, [])
const counter = useSelector(state => state.counter)
const dispatch = useDispatch()
const getCounter = () => {
dispatch(get_data())
}
return (
<div className="container-fluid mt-3">
<p className="text-white">Counter: {counter}</p>
<button onClick={() => dispatch(increment(4))} className="btn btn-primary">+</button>
<button onClick={() => dispatch(decrement())} className="btn btn-danger">-</button>
</div>
)
The problem is that in both your reducers, you're returning the default state in the default case. This means that every non handled action will reset the states.
For instance, when you dispatch an increment action, it goes in the default state in your reducer and sets the state to false.
The default case should simply return state unchanged.
Besides that, you're useEffect seems a bit dangerous, as getCounter changes at every render, it will be called every time.
I would also advise you to use a middleware for redux if you want to save something in the localStorage. A reducer is supposed to be a function with no side-effects so you're not respecting the rule here.
You should also not read from the localStorage via an action but instead read from the localStorage when creating your store.
I'm using React and Redux and storing data in a loggedUser variable upon user login.
my login reducer looks like this:
const loginReducer = (state = null, action) => {
switch (action.type) {
case "SET_USER":
if (action.data) userService.setToken(action.data.token);
return action.data;
default:
return state;
}
};
export const fetchUser = () => {
return dispatch => {
const userStr = window.localStorage.getItem("loggedVintageUser");
const user = JSON.parse(userStr);
if (user) {
dispatch({ type: "SET_USER", data: user });
}
};
};
export const setUser = data => {
return dispatch => {
dispatch({ type: "SET_USER", data });
};
};
export const login = data => {
return async dispatch => {
const user = await loginService.login({
username: data.username,
password: data.password
});
window.localStorage.setItem("loggedVintageUser", JSON.stringify(user));
dispatch({ type: "SET_USER", data: user });
};
};
In my core App component i'm dispatching the fetchUser and setUser creators
useEffect(() => {
fetchUser();
}, [props.fetchUser]);
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem("loggedVintageUser");
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON);
props.setUser(user);
userService.setToken(user.token);
}
}, []);
I'm displaying a list of favorite items for a user and when i go to refresh the page, i'm getting the following error:
TypeError: Cannot read property 'favorites' of null
Here is relevant code for my Favorites component. The error is triggered on the loggedUser.favorites data. I can see when visiting the favorites page, the loggedUser field is there and data displays fine but on refresh the loggedUser variable turns to null.
const searchCards = ({ loggedUser, search }) => {
const favorites = loggedUser.favorites;
console.log("FAVORITES", favorites);
return search
? favorites.filter(a =>
a.title
.toString()
.toLowerCase()
.includes(search.toLowerCase())
)
: favorites;
};
const Cards = props => {
useEffect(() => {
setData(props.cardsToShow);
}, [props]);
const [filteredData, setData] = useState(props.cardsToShow);
const mapStateToProps = state => {
return {
baseball: state.baseball,
loggedUser: state.loggedUser,
page: state.page,
entries: state.entries,
query: state.query,
pageOutput: state.pageOutput,
search: state.search,
cardsToShow: searchCards(state)
};
};
const mapDispatchToProps = {
searchChange,
fetchData,
updateUser
};
I tried to add this before i render the data, but it's not working
if (!props.loggedUser) return null;
How can i retain that state if a user is refreshing the page. The odd part is that on my home page where i have a similar sort of display a refresh isn't causing the same problems.
check once loggedUser is exist in state or not. Print state using console.log(state). you may also open inspect tool and go to application tab and click on local storage, you will get localStorage data.
Well, i figured this out and got some help from this post here. Redux store changes when reload page
My loggedUser state was disappearing after reload, so i just loaded the inital state for loggedUser pulling the data from the local storage:
function initState() {
return {
token: localStorage.token,
firstName: localStorage.firstName,
id: localStorage.id,
favorites: localStorage.favorites,
username: localStorage.username
};
}
const loginReducer = (state = initState(), action) => {
switch (action.type) {
case "SET_USER":
if (action.data) userService.setToken(action.data.token);
return action.data;
default:
return state;
}
};
I want to implement an action which gets item by id, so I've created fetchItemAction(), as follows:
export const fetchItemAction = () => (dispatch) => {
dispatch({
type: FETCH_ITEM_REQUEST,
});
return axios.get(`${url}/notes/5d4724cd62087b0e141f75a4`)
.then(({ data }) => {
console.log(data);
dispatch({
type: FETCH_ITEM_SUCCESS,
data,
});
})
.catch((error) => {
dispatch({
type: FETCH_ITEM_FAILURE,
});
});
};
Then, I try to set item field in State in my reducer:
const initialState = {
isAuthenticated: false,
user: {},
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_ITEM_REQUEST:
return {
...state,
isLoading: true,
};
case FETCH_ITEM_SUCCESS:
return {
...state,
item: action.data,
isLoading: false,
};
}
};
Then, I try to get those data in Details component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchItemAction } from 'actions/actions';
class Details extends Component {
componentDidMount() {
const { fetchItem } = this.props;
fetchItem();
}
render() {
const { item, isLoading } = this.props;
return (
<>
{console.log(item)}
{/* <p>{item.title}</p> */}
</>
);
}
}
const mapStateToProps = ({ item, isLoading }) => ({ item, isLoading });
const mapDispatchToProps = dispatch => ({
fetchItem: () => dispatch(fetchItemAction()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Details);
As a result, I'm getting following data in console:
Apart from two undefinded the result looks good because there is correct response from my backend.
But, when I try to uncomment <p>item.title</p> line in Details.js, the app crash:
TypeError: Cannot read property 'title' of undefined
I also implemented correctly fetchItemsAction(), addItemAction() and deleteItemAction() which are very similar but I have no idea what is wrong in fetchItemAction().
This is an asynchronous issue. componentDidMount is called when the component is mounted. Then, you're calling fetch. So on your first render, item is undefined. Once the data is returned, the render is triggered again with item data.
So, just check if item is defined:
render() {
const { item, isLoading } = this.props;
return (
<>
{console.log(item)}
{item && <p>{item.title}</p>}
</>
);
}