Object passed changes every render - javascript

I am trying to define a global state provider for an app I am building with react. But I keep getting the error
The object passed as the value prop to the Context provider (at line 19) changes every render. To fix this consider wrapping it in a useMemo hook
Here is my file structure. state.ts
export default interface State {
data: boolean
}
export const initialState: State = {
data: false,
}
action.ts
type Action = {
type: "SET_DATA"
value: boolean
}
export default Action
context.ts
import { Context, createContext, Dispatch } from "react"
import Action from "./actions"
import State, { initialState } from "./state"
const GlobalContext: Context<{
globalState: State
dispatch: Dispatch<Action>
// eslint-disable-next-line #typescript-eslint/no-unused-vars
}> = createContext({ globalState: initialState, dispatch: (_: Action) => {} })
export default GlobalContext
provider.tsx
import * as React from "react"
import { ReactNode, ReactElement, useReducer } from "react"
import GlobalContext from "./context"
import Reducer from "./reducer"
import State, { initialState as defaultInitialState } from "./state"
export default function GlobalStateProvider({
children,
initialState = defaultInitialState,
}: {
children: ReactNode
initialState?: State
}): ReactElement {
const [globalState, dispatch] = useReducer(Reducer, initialState)
return (
<GlobalContext.Provider value={{ dispatch, globalState }}>
{children}
</GlobalContext.Provider>
)
}
GlobalStateProvider.defaultProps = {
initialState: defaultInitialState,
}
I have gone through the code multiple times and I cannot seem to figure out what is wrong and why I am getting this error.
If someone can further explain why this is happening and perhaps and solution that would be helpful.

This is not really a syntax error, meaning it won't break the app, but a linting error, where some linters are trying to enforce best standards. You can change linter settings to provide warnings instead of throwing errors.
If you want to fix this, you can wrap the context values in a useMemo hook as the linter suggested.
const globalContextValue = useMemo(
() => ({
dispatch, globalState
}),
[dispatch, globalState]
);
return (
<GlobalContext.Provider value={globalContextValue}>
{children}
</GlobalContext.Provider>
)
Note that there are controversies around this linting rule, some say that its outdated and unnecessary, so some would suggest to disable the rule entirely.

Related

What to set as the Provider's value using react typescript

I'm trying to learn how to use hooks and context in React, but need to work with TypeScript (new to that as well).
I am confused about what to set provider value on my below code. I have created a hook useFirebase it will return some functions like RegisterUser, SignIn, user(it's a state), logout.
First I created UseAuth.tsx hook, using this hook I want to import the above functions which I mentioned.
import { useContext } from 'react';
import { AuthContext } from '../context/AuthProvider';
const UseAuth = () => {
const auth = useContext(AuthContext)
return auth
};
export default UseAuth;
Everything is ok but I am getting an error on the provider value. The error is (JSX attribute) React.ProviderProps<null>.value: null Type '{ RegisterUser: RegisterUserFunction; SignIn: SignInUserFunction; user: userState; logout: () => void; }' is not assignable to type 'null'. The expected type comes from property 'value' which is declared here on type 'IntrinsicAttributes & ProviderProps<null>' I have created an interface IAuth but where I need to define it I am confused.
import React, { createContext } from 'react';
import { UseFirebase } from '../hooks/UseFirebase';
export const AuthContext = createContext(null)
export interface IAuth {
RegisterUser: () => void;
SignIn : () => void;
user : any
logout : () => void;
}
const AuthProvider = ({ children } :any) => {
const allContext = UseFirebase()
return (
<AuthContext.Provider value={allContext}>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;
The problem is in
export const AuthContext = createContext(null)
You need to pass a proper type to it, so it won't complain when trying to assign any value.
Try something like
React.createContext<IAuth>(initialValue);
For a more detailed explanation check out this article react-context-with-typescrip

Redux hook useSelector not giving proper state

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;

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 connect() fails to pass the props

I'm trying to learn react-redux architecture, and I failed on the most basic stuff.
I created class HomePage and used react-redux connect() to connect it to store's state and dispatch.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {HomeButtonClickAction} from "./HomeActionReducer";
import {connect} from "react-redux";
class HomePage extends Component {
constructor(props) {
super(props);
console.log('HomePage props');
console.log(this.props);
this.buttonClicked = this.buttonClicked.bind(this);
}
buttonClicked() {
console.log('button cliked');
this.props.buttonClick();
}
render() {
console.log('Re-rendering...');
let toggleState = this.props.toggle ? 'ON' : 'OFF';
return (
<div>
<button onClick={this.buttonClicked}>{ toggleState }</button>
</div>
)
}
}
HomePage.propTypes = {
toggle: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired
};
const mapStateToProps = (state, ownProps) => {
return {
toggle: state.toggle
}
};
const mapDispatchToProps = (dispatch, ownProps) => {
return {
buttonClick: () => {
dispatch(HomeButtonClickAction());
}
}
};
const HomeContainer = connect(
mapStateToProps,
mapDispatchToProps
)(HomePage);
export default HomePage;
But it's not working for me. HomeContainer doesn't pass props to HomePage component.
I've got these warnings in devtools.
My index.js looks like this.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import AppReducer from "./reducers/AppReducer";
import { createStore } from "redux";
import { Provider } from 'react-redux';
const store = createStore(AppReducer);
ReactDOM.render(
<Provider store={ store }>
<App/>
</Provider>,
document.getElementById('root')
);
registerServiceWorker();
and AppReducer.js
import { combineReducers } from 'redux';
import { toggle } from '../home/HomeActionReducer';
const AppReducer = combineReducers({
toggle
});
export default AppReducer;
and HomeActionReducer.js
const HOME_BUTTON_CLICK = 'HOME_BUTTON_CLICK';
export function toggle (state = true, action) {
console.log('toggle launched');
switch (action.type) {
case HOME_BUTTON_CLICK :
return !state;
default:
console.log('Toggle reducer default action');
return state;
}
}
export function HomeButtonClickAction() {
console.log('action emitted');
return {
type: HOME_BUTTON_CLICK
};
}
Being a newbie I'll really appreciate your help :)
You are exporting HomePage, which is the presentational component. You want to export HomeContainer, which is the container that passes the props to HomePage through connect.
So replace this
export default HomePage;
with this
export default HomeContainer;
You can also directly write
export default connect(mapStateToProps, mapDispatchToProps)(HomePage);
Note that, since it's the default export, you can name the import as you want, eg.:
import HomePage from './HomePage' // even if it's HomeContainer that is exported
You have this:
const HomeContainer = connect(
mapStateToProps,
mapDispatchToProps
)(HomePage);
export default HomePage;
To create an instance of the connect component you need to do this:
export default connect()(HomePage);
Notice I did not write export default twice, bad practice, you only export default once per component so the connect() goes inside that same line of code and the invocation or second set of parentheses you wrap around the component you are working in.
This connect() function is actually a React component that you are going to pass some configuration to and the way you begin to do that is by calling mapStateToProps like so:
const mapStateToProps = () => {
};
export default connect()(HomePage);
You could also do:
function mapStateToProps() {
}
If you read it, it makes sense, this is saying that we are going to map our state object, all the data inside the redux store and run some computation that will cause that data to show up as props inside our component, so thats the meaning of mapStateToProps.
Technically, we can call it anything we want, it does not have to be mapStateToProps, but by convention we usually call it mapStateToProps and its going to be called with all the state inside of the redux store.
const mapStateToProps = (state) => {
};
export default connect()(HomePage);
The state object contains whatever data you are trying to access from the redux store. You can verify this by console logging state inside the function like so:
const mapStateToProps = (state) => {
console.log(state);
return state;
};
export default connect()(HomePage);
I am returning state just to ensure that everything is working just fine.
After defining that function, you take it and pass it as the first argument to the connect() component like so:
const mapStateToProps = (state) => {
console.log(state);
return state;
};
export default connect(mapStateToProps)(HomePage);
Thats how we configure the connect component.We configure it by passing it a function. Run that and see what happens.

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