Redux hook useSelector not giving proper state - javascript

I am trying to get Redux state out of the global store and when I use useSelector in a WithAuth HOC I am not getting the proper state. On my global state there are two keys: currentUser and currentUser.isAuthenticated.
In my Redux dev tools, the isAuthenticated key shows having the value true, but when I make use of the hook, I get false.
I've had a look at the official documentation, but I couldn't find anything which might help me, if I've read it correctly.
Could someone tell me what I'm doing wrong? I am new to using hooks for Redux. Thanks.
withAuth.tsx
import React,
{ useEffect } from "react";
import { useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
import { Routes } from "../../constants/RoutesNames";
import { DefaultRootState } from "../../store/reducers";
interface Props
{
ComponentToBeRendered: React.FC<unknown>
props?: any
};
const stateSelector = (state: DefaultRootState) => state.currentUser.isAuthenticated;
const WithAuth : React.FC<Props> = ({ ComponentToBeRendered, props }) =>
{
const isAuthenticated = useSelector<DefaultRootState, boolean>(stateSelector);
const history = useHistory();
useEffect(() =>
{
if (!isAuthenticated) history.push(Routes.SIGN_IN);
}, [ isAuthenticated, history ]);
return (
<ComponentToBeRendered {...props} />
)
}
export default WithAuth;

What do you mean by the wrong value? Could you add more info to the problem?
Also remember you can shorthand the return of a statement like this to keep code cleaner:
const stateSelector = (state: DefaultRootState) => state.currentUser.isAuthenticated

It turns out the useSelector hook does a double take on the Redux state. If you console.log (console.log("Got isAuth", isAuthenticated);) inside the useEffect hook you will see that the first log will be Got isAuth false and the second Got isAuth true.
I don't know if that's the whole story, but for now I've re-implemented WithAuth to accept a rendered element rather than a component to be rendered. And then I conditionally return that element or my sign in page rendered. Thus,
import React from "react";
import { useSelector } from "react-redux";
import AuthFlow from "../../pages/AuthFlow";
import { DefaultRootState } from "../../store/reducers";
interface Props
{
ComponentToBeRendered: React.ReactElement;
};
const isAuthenticatedSelector = (state: DefaultRootState) => state.currentUser.isAuthenticated;
const WithAuth : React.FC<Props> = React.memo(({ ComponentToBeRendered }) =>
{
const isAuthenticated = useSelector<DefaultRootState, boolean>(isAuthenticatedSelector);
return (
isAuthenticated ? ComponentToBeRendered : <AuthFlow />
)
})
export default WithAuth;
I don't know if this is the best approach, but I've found a workaround for now. Strange that useSelector can't just get the Redux state correctly on the first run. Maybe I'm missing something someone can show me.
=========== EDIT ===========
I thought a little more about this and I think this approach might be slightly better for various reasons
import React from "react";
import { useSelector } from "react-redux";
import BasicTextLink from "../../components/BasicLink/BasicLink";
import { Routes } from "../../constants/RoutesNames";
import { DefaultRootState } from "../../store/reducers";
interface Props
{
ElementToBeShown: React.ReactElement;
};
const isAuthenticatedSelector = (state: DefaultRootState) => state.currentUser.isAuthenticated;
const WithAuth : React.FC<Props> = React.memo(({ ElementToBeShown }) =>
{
const isAuthenticated = useSelector<DefaultRootState, boolean>(isAuthenticatedSelector);
return (
isAuthenticated ? ElementToBeShown : <h2>You are not signed in. Please {
<BasicTextLink baseColor useHoverEffect darkHover to={Routes.SIGN_IN} underline={"hover"}>
sign in
</BasicTextLink>
} first</h2>
)
});
export default WithAuth;

Related

Data is fetched and stored into redux but when accessing it comes undefined

I am making api call in my redux action creator storing those data into my redux state, but when trying to access that using useSelector getting undefined.
Not able to understand where i am doing wrong. Any help here would be great. I am using redux toolkit.
Attaching snipped for reducers, action creator, component, redux state snapshot from redux dev tools and debugging logs
Note: appID will come from path params but for now i am hardcoding it
In my SuggestedAudience snipped if i comment out {app.name} which is causing the problem then my redux states are loaded correctly, snapshot attached
Observation: If i am making any changes to Suggested audience and saving then i am getting value but when reloading it 2-3 times again the error comes.
//apps slice to store apps
import { createSlice } from "#reduxjs/toolkit";
import _ from "lodash";
const appsSlice = createSlice({
name: "apps",
initialState: {},
reducers: {
appSuccess(state, action) {
const apps = _.keyBy(action.payload.data, "uuid");
return apps;
},
},
});
export const appsAction = appsSlice.actions;
export default appsSlice.reducer;
//ui-slice to store app data
import { createSlice } from "#reduxjs/toolkit";
const uiSlice = createSlice({
name: "ui",
initialState: { appId: null, orgId: null },
reducers: {
setAppId(state, action) {
state.appId = action.payload;
},
},
});
export const uiAction = uiSlice.actions;
export default uiSlice.reducer;
//App.js
import logo from "./logo.svg";
import "./App.css";
import PageHeader from "./components/PageHeader";
import SuggestedAudiences from "./components/SuggestedAudiences";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { uiAction } from "./components/store/ui-slice";
import { appsAction } from "./components/store/apps-slice";
import { fetchApps } from "./components/store/apps-action";
function App() {
const dispatch = useDispatch();
const appID = "52f14657d6c765401a2d7d4-41dfb38a-4262-11e3-9166-005cf8cbabd8";
useEffect(() => {
dispatch(uiAction.setAppId(appID));
dispatch(fetchApps());
}, []);
return (
<>
<PageHeader />
<SuggestedAudiences />
</>
);
}
export default App;
//Suggested Audiences
import React, { useEffect } from "react";
import { fetchApps } from "./store/apps-action";
import { useDispatch, useSelector } from "react-redux";
import { uiAction } from "./store/ui-slice";
const SuggestedAudiences = (props) => {
const dispatch = useDispatch();
const appId = useSelector((state) => state.ui.appId);
const app = useSelector((state) => state.apps[appId]);
return (
<>
<div>Suggested Audiences 🚀</div>
{app.name}
</>
);
};
export default SuggestedAudiences;
Fixed it by doing {app && app.name} or {app?.name} would also work.
Keeping it open if someone can help me understand it why it is happening and is there any other way to fix this so that undefined doesn't come in first place itself.
Try to change this line
const app = useSelector((state) => state.apps[appId]);
into this
const app = useSelector((state) => state?.apps[appId]);

React Redux & ContextApi - How to pass a prop via context and keeping it "connected"?

Considering the following project setup on a react-redux application that uses context API to avoid prop drilling. The example given is simplified.
Project Setup
React project uses React Redux
Uses context API to avoid prop drilling in certain cases.
Redux store has a prop posts which contains list of posts
An action creator deletePost(), which deletes a certain post by post id.
To avoid prop drilling, both posts and deletePosts() is added to a context AppContext and returned by a hook funciton useApp().
posts array is passed via contexts so it is not used by connect() function. Important
Problem:
When action is dispatched store is updated however Component is not re-rendered (because the prop is not connected?). Of course, if I pass the prop with connect function and drill it down to child rendering works fine.
What is the solution?
Example Project
The example project can be found in codesandbox. Open up the console and try to click the delete button. You will see no change in the UI while you can see the state is updated in the console.
Codes
App.js
import Home from "./routes/Home";
import "./styles.css";
import { AppProvider } from "./context";
export default function App() {
return (
<AppProvider>
<div className="App">
<Home />
</div>
</AppProvider>
);
}
context.js
import { useDispatch, useStore } from "react-redux";
import { useContext, createContext } from "react";
import { deletePost } from "./redux/actions/posts";
export const AppContext = createContext();
export const useApp = () => {
return useContext(AppContext);
};
export const AppProvider = ({ children }) => {
const dispatch = useDispatch();
const {
posts: { items: posts }
} = useStore().getState();
const value = {
// props
posts,
// actions
deletePost,
dispatch
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
Home.js
import { connect } from "react-redux";
import Post from "../components/Post";
import { useApp } from "../context";
const Home = () => {
const { posts } = useApp();
return (
<section>
{posts.map((p) => (
<Post key={p.id} {...p} />
))}
</section>
);
};
/*
const mapProps = ({ posts: { items: posts } }) => {
return {
posts
};
};
*/
export default connect()(Home);
Post.js
import { useApp } from "../context";
const Post = ({ title, content, id }) => {
const { deletePost, dispatch } = useApp();
const onDeleteClick = () => {
console.log("delete it", id);
dispatch(deletePost(id));
};
return (
<article>
<h1>{title}</h1>
<p>{content}</p>
<div className="toolbar">
<button onClick={onDeleteClick}>Delete</button>
</div>
</article>
);
};
export default Post;
You're not using the connect higher order component method properly . Try using it like this so your component will get the states and the function of your redux store :
import React from 'react';
import { connect } from 'react-redux';
import { callAction } from '../redux/actions.js';
const Home = (props) => {
return (
<div> {JSON.stringify(props)} </div>
)
}
const mapState = (state) => {
name : state.name // name is in intialState
}
const mapDispatch = (dispatch) => {
callAction : () => dispatch(callAction()) // callAction is a redux action
//and should be imported in the component also
}
export default connect(mapState , mapDispatch)(Home);
You can access the states and the actions from your redux store via component props.
Use useSelector() instead of useState(). Example codepen is fixed.
Change from:
const { posts: { items: posts } } = useStore().getState();
Change to:
const posts = useSelector(state => state.posts.items);
useStore() value is only received when component is first mounted. While useSlector() will get value when value is changed.

Next.JS Redux dispatch not working in getStaticProps()

I am pretty new to Next.JS and I was trying to set up Redux with my Next.JS application. Now my page is supposed to display a list of posts that I am calling in from an API. The page renders perfectly when I'm dispatching from useEffect() to populate the data on to my page, but getStaticProps() or getServerSideProps() are not working whatsoever!
Here is a bit of code that will give you a hint of what I've done so far:
store.js
import { useMemo } from 'react'
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'
import rootReducer from './reducers/rootReducer'
const initialState = {}
const middlewares = [thunk]
let store
function initStore(preloadedState = initialState) {
return createStore(
rootReducer,
preloadedState,
composeWithDevTools(applyMiddleware(...middlewares))
)
}
export const initializeStore = (preloadedState) => {
let _store = store ?? initStore(preloadedState)
if (preloadedState && store) {
_store = initStore({
...store.getState(),
...preloadedState,
})
store = undefined
}
if (typeof window === 'undefined') return _store
if (!store) store = _store
return _store
}
export function useStore(initialState) {
const store = useMemo(() => initializeStore(initialState), [initialState])
return store
}
action.js
export const fetchPosts = () => async dispatch => {
const res = await axios.get('https://jsonplaceholder.typicode.com/posts')
dispatch({
type: FETCH_POSTS,
payload: res.data
})
}
_app.js
import { Provider } from 'react-redux'
import { createWrapper } from 'next-redux-wrapper'
import { useStore } from '../redux/store'
export default function MyApp({ Component, pageProps }) {
const store = useStore(pageProps.initialReduxState)
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
)
}
These are the files that I needed for the basic redux setup. Once my store was set up and I wrapped my app around the Provider, I initially though of using useEffect() hook to populate data on a component that was rendering inside my index.js file.
component.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchPosts } from '../redux/actions/postsAction'
const Posts = () => {
const dispatch = useDispatch()
const { items } = useSelector(state => state.posts)
useEffect(() => {
dispatch(fetchPosts())
}, [])
return (
<div className="">
<h1>Posts</h1>
{items.map(post => {
return (<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>)
})}
</div>
)
}
export default Posts
This worked perfectly! All my posts were showing up inside the component. The problem occurred when I was trying to achieve the same behaviour with server side rendering (or even SSG). I wanted to populate the data during the pre-render phase but for some reason the items array which is supposed to hold all the data is empty, basically meaning that the disptacher was never called! Here is the piece of code that is bothering me (exactly same as previous code, but this time I'm using getStaticProps() instead of useEffect()):
component.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchPosts } from '../redux/actions/postsAction'
const Posts = ({ items }) => {
return (
<div className="">
<h1>Posts</h1>
{items.map(post => {
return (<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>)
})}
</div>
)
}
export async function getStaticProps() {
console.log('Props called')
const dispatch = useDispatch()
const { items } = useSelector(state => state.posts)
dispatch(fetchPosts())
console.log(items)
return { props: { items } }
}
export default Posts
By running this, I'm getting an error that items is empty! Please help me, I have no clue what's going wrong here.
Well I fixed this issue myself but I forgot to post an answer for it, my bad!
The problem here really is very simple, hooks don't work outside of a functional component!
I think, inside of getStaticProps just call API or get datas from DB and returns it as props to pages/index.js (any component you want) and inside of this component we can get datas from getStaticProps as props.
Also we can set it as global state using useDispatch of react-redux. After that any component we can call those states using redux mapStateToProps. This is my solution.
This maybe a solution if anyone faced this problem,
import React from 'react';
import {useSelector} from 'react-redux';
import {wrapper} from '../store';
export const getStaticProps = wrapper.getStaticProps(store => ({preview})
=> {
console.log('2. Page.getStaticProps uses the store to dispatch things');
store.dispatch({
type: 'TICK',
payload: 'was set in other page ' + preview,
});
});
// you can also use `connect()` instead of hooks
const Page = () => {
const {tick} = useSelector(state => state);
return <div>{tick}</div>;
};
export default Page;
Got it from here: https://github.com/kirill-konshin/next-redux-wrapper

React presentational component unable to read value for <input /> from redux store using react container

I'm new to stackoverflow and quite new to using react/redux. I've been scanning over quite a few posts already to see if a similar post could provide me with an answer but I'm still left puzzled.
I currently have a presentational component "Repetitions" and a container component to get props from redux store and dispatch actions from the presentational component to redux store. I have the presentational component updating the redux store when I enter data into the input field but I am wanting to use the redux store to retrieve the input value so that when a user first comes on to the page the input value is "0" as that is the initial value inside the redux store.
I originally made a simple Counter component using react/redux and it was working ok. I have since made the "Repetition" component and altered the redux store to use a combinedreducer and this is when the problems seemed to start as neither components can read from the redux store.
Rootreducer.ts
import { combineReducers } from "redux";
import countReducer from "./example/reducer";
import repetitionsReducer from "./reps/reducer";
const rootReducer = combineReducers({
countReducer,
repetitionsReducer
})
export default rootReducer;
RepetitionsReducer.ts
import { RepetitionsState } from "../types";
import { AddRepetitionsAction } from "./actions";
export type RepetitionsActionType = AddRepetitionsAction;
export type Dispatch = (action: RepetitionsActionType) => void;
// The reducer updates the count
const initialState: RepetitionsState = {
repetitions: 0
};
const repetitionsReducer = (
state = initialState,
action: RepetitionsActionType
): RepetitionsState => {
switch (action.type) {
case "ADD_REPETITIONS":
return { ...state, repetitions: action.repetitions };
default:
return state;
}
}
export default repetitionsReducer;
RepetitionsContainer.ts
import { connect } from "react-redux";
import { RootState } from "../../store/types";
import { Dispatch } from "../../store/reps/reducer";
import { addRepetitions } from "../../store/reps/actions";
import Repetitions from "../../components/reps/Repetitions";
interface StateFromProps {
repetitions: number ;
}
interface DispatchFromProps {
updateRepetitions: (repetitions: number) => void;
}
export type RepetitionsProps = StateFromProps & DispatchFromProps;
const mapStateToProps = (state: RootState): StateFromProps => ({
repetitions: state.repetitions
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchFromProps => ({
updateRepetitions: (repetitions: number) => dispatch(addRepetitions(repetitions))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Repetitions);
RepetitionsComponent.ts
note: When I try to console.log "repetitions" I am getting undefined at the moment.
import React from "react";
import { RepetitionsProps } from "../../containers/reps/Repetitions";
const Repetitions: React.FunctionComponent<RepetitionsProps> = ({
repetitions,
updateRepetitions
}) => {
console.log(repetitions)
return (
<div>
<h3>Reps</h3>
<input
onChange={(event) => updateRepetitions(Number(event.target.value))}
value={ repetitions } // <-- This is the value i'm wanting to present to the user from the redux store
/>
</div>
);
};
export default Repetitions;
App.ts
import React from "react";
import ReactDOM from "react-dom";
import * as serviceWorker from "./serviceWorker";
import Header from "./components/header/Header";
import { Provider } from "react-redux";
import { createStore } from "redux";
import Counter from "./containers/example/Counter";
import Repetitions from "./containers/reps/Repetitions";
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from "./store/reducer";
const store = createStore(rootReducer, composeWithDevTools());
console.log(store.getState())
function App() {
return (
<div className="App">
<Header title={"Rep count"} />
<Repetitions />
<br />
<br />
<br />
<Counter />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
The expected results I would be hoping to see would be a "0" presented in the input box underneath the "Reps" header when a user first loads the page. Instead the box is empty but the redux store shows the value for repetitions as "0".
reps-input-desired-results
It is also worth noting that the counter below the input field used to read "0" from the redux store when I first loaded the page however now it is also undefined.
Thank you for taking the time to look at my post. Any help would be greatly appreciated!
Hmmm... something is wrong here:
First of all, your state for repetition is currently holding an object. It should hold a simple number.
Secondly, the name of the repetition state on the store (from the snapshot you've attached) is "repetitionReducer" and not "repetition" as you try to fetch it in mapStateToProps:
const mapStateToProps = (state: RootState): StateFromProps => ({
repetitions: state.repetitions // <- this one here...
});
Hope this helps :)

React/Redux - Why is my action/reducer not working?

Newbie here. Trying to get my Redux action to talk to my reducer and update the store. - TL;DR question further down:
Context: I am only really getting issues now that i'm trying to split out my actions/reducers into separate files and folders. Prior to this i got everything working, when it was all in one file, sort of thing.
So... I have 3 files.
i) A Client.js (top level, where my store is), which takes my reducer in and then uses provider to get the store to <Main />
Main.js then uses mapDispatchToProps to get a toggleLogin action(creator) into its onClick prop.
This is then passed down to <LoginButton /> where onClick can be clicked. And when it is, i can get a console log from the toggleLogin action-creator. But any attempt to return { type: 'TOGGLE_LOGIN' } sees either a:
'ReferenceError: store is not defined' or no change... and i have to console.log either side to find the problem sits within that return {} area. So...
TL;DR Question why wont my reducer pick-up that action and update the store? Its driving me nutty.
ClientJS:
import React from 'react';
import ReactDOM from 'react-dom';
import Redux from 'redux';
import Main from './Components/Main'
import { myReducer } from './Reducers/reducer';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
const store = createStore(myReducer);
ReactDOM.render(
<Provider store={store}>
<Main />
</Provider>,
document.getElementById('root')
);
Main.Js
import React from 'react';
import LoginButton from './LoginButton';
import PlayButton from './PlayButton';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { toggleLogin } from '../actions/toggleLogin';
const Main = (props) => {
const { onClick} = props;
return (
<div>
<h1>hello world</h1>
<LoginButton onClick={onClick} />
<PlayButton />
</div>
);
};
const mapStateToProps = (state) => {
return {
isLoggedIn: state.isLoggedIn
}
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
onClick: toggleLogin,
}, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(Main);
reducer.js:
export const myReducer = (state = { isLoggedIn: false }, action) => {
switch (action.type) {
case 'TOGGLE_LOGIN':
return {
isLoggedIn: !action.isLoggedIn
}
default:
return state;
}
};
LoginButton.js
import React from 'react';
import Redux from 'redux';
const LoginButton = (props) => {
const { onClick } = props;
return (
<div>
<button onClick={onClick}>LOGIN/LOGOUT</button>
</div>
);
};
export default LoginButton;
toggleLogin.js (action creator):
export function toggleLogin() {
console.log('this works');
store.dispatch({
type: 'TOGGLE_LOGIN',
});
console.log('this doesnt work');
}
toggleLogin should just return {type: 'TOGGLE_LOGIN'}. bindActionCreators is what wraps that function in a store.dispatch call, so you are basically trying to dispatch what another dispatch returns.
Also you get that 'ReferenceError: store is not defined' exception because inside your function there is no store reference.
Just closing the loop on this one - looks like it was a mesh of things:
i) my TOGGLE_LOGIN was indeed trying to dispatch. It should've just been returning an object.
ii) i did need bindActionCreators but it would've been more appropriate with more than one action.
iii) most importantly - when i was changing my TOGGLE_LOGIN to return an object... i was putting two console logs either side of it, for debugging purposes. But being a newbie/absent minded, i totally overlooked that this was effectively putting a function after a return statement. Once i took the console-logs off, i was up and running. Thanks everyone.

Categories