I have simple input which will be available in two different components so to share the state of this input, I decided to use reducer.
Here is my solution:
index.js
.....
const Index = () => {
const store = createStore(rootReducer, applyMiddleware(thunk));
.............
Reducer
const initialState = {
inputValue: "Testing.com"
}
const nameReducerr = (state = initialState, action) => {
switch (action.type) {
case "INPUT_CHANGE":
return Object.assign({}, state, {inputValue: action.text})
default:
return state
}
}
export default nameReducerr
here is my component
import React, {useState} from 'react'
import {useSelector, useDispatch } from "react-redux"
function inputData() {
const [name, setName] = useState('');
const inputValue = useSelector(state => state.inputValue);
const dispatch = useDispatch();
const handleKeyDown = (event) => {
if (event.key === "Enter") {
dispatch(setName(event.target.value));
}
};
console.log('input value', inputValue);
return (
<div>
<input
onKeyDown={handleKeyDown}
type="text"
className="form-control address"
name=""
/>
<h1>Name: {name}</h1>
<h1>Input Value: {inputValue}</h1>
</div>
)
}
export default input data
Unfortunately, I get the following error.
Error: Actions must be plain objects. Use custom middleware for async actions.
What am I doing wrong here? thx
setName is your setter for your local react state and has nothing to do with redux - so it's result cannot be dispatched. You will need to write an action creator instead.
Generally, useState is local component state. So it also does not select the value from the redux store in any way, which will be your next problem. You will need to use useSelector instead.
Also, you are using a very outdated style of redux here which we do not really recommend any more. To learn modern redux, please follow the official tutorials over at https://redux.js.org/tutorials/index - you will write a lot less and more secure code in the end.
Related
What am I trying to do?
Using Redux Toolkit, I'm trying to access the "store" for a value, specifically "username" which I've created a "slice" for, from a non-React file called SomeFile.js.
What is the code that currently tries to do that?
// userMetadataSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
username: "",
};
const userMetadataSlice = createSlice({
name: "userMetadata",
initialState,
reducers: {
updateUsername: (state, action) => {
const username = action.payload;
state.username = username;
},
},
});
export const { updateUsername } = userMetadataSlice.actions;
export default userMetadataSlice.reducer;
export const selectUsername = (state) => {
return state.userMetadata.username;
}
// SomeFile.js
import { selectUsername } from "../redux/userMetadataSlice";
import { useSelector } from "react-redux";
export const displayUsername = () => {
const username = useSelector(selectUsername);
console.log("Username:", username); // Error.
}
What do I expect the result to be?
To be able to pull the username from the "store".
What is the actual result?
When I try to access the value via "useSelector" from the non-react file an error occurs: React Hook "useSelector" is called in function "selectUsername" which is neither a React function component or a custom React Hook function
What I think the problem could be?
SomeFile.js does not have anything React related within it because it just pulls data from the store and outputs the data.
A solution I've tried that worked was to do this:
// userMetadataSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
username: "",
};
const userMetadataSlice = createSlice({
name: "userMetadata",
initialState,
reducers: {
updateUsername: (state, action) => {
const username = action.payload;
state.username = username;
},
},
});
export const { updateUsername } = userMetadataSlice.actions;
export default userMetadataSlice.reducer;
export const selectUsername = (state) => {
return state.userMetadata.username;
}
// New code here!
export function SelectUsername() {
const username = useSelector(selectUsername);
return username;
}
// SomeFile.js
import { SelectUsername } from "../redux/userMetadataSlice";
export const displayUsername = () => {
console.log("Username:", SelectUsername); // No errors, shows correct output.
}
The solutions I'm looking for is this:
Is my proposed solution the "proper" way to receive info from the "store" in non-React files?
Is there a custom hook solution for this?
Is my proposed solution the "proper" way to receive info from the
"store" in non-React files?
No, it's abusing the Rules of Hooks and React functions. You are directly invoking the SelectUsername React function.
Is there a custom hook solution for this?
No, React hooks work only in React functions and custom React hooks.
You can access your state from your Redux store object.
Store
From your created store object you'll have a getState method to invoke.
getState()
Returns the current state tree of your application. It is equal to the
last value returned by the store's reducer.
Returns
(any): The current state tree of your application.
You can export your created store object for import into non-React JS files and they can invoke the getStore method.
import store from '../path/to/store';
...
const state = store.getState();
The useSelector React hook from react-redux won't work outside a React component, but the selectUsername state selector function will.
// SomeFile.js
import store from '../path/to/store';
import { selectUsername } from "../redux/userMetadataSlice";
...
export const displayUsername = () => {
const state = store.getState();
const username = selectUsername(state);
console.log("Username:", username);
return username;
};
See the other Store Methods for subscribing to state changes and dispatching actions to your store from outside React.
The page does not render, citing TypeError: state is undefined, tracing back to this line in SelectForm.js: const filter = useSelector(state => state.filter);.
I've spent hours trying to figure out what I'm doing wrong. I've tried createSelector but that didn't work. I've tried dispatching a "Fetch Initial State" action, and that didn't work. The component is wrapped in provider tags. I'm not sure why I don't have access to the state. At this point I'm unable to see any flaws I've been looking at it for so long.
Code Snippets
reducer.js
let initialState = {
filter: {
country: null,
state: null,
level: null,
team: null
},
isDBConnecting: false,
isDBConnected: false,
isDBError: false
}
const SelectorReducer = (state=initialState, action) => {
switch (action.type) {
case 'DB_CONNECT_INIT':
return {
...state,
isDBConnecting: true,
isDBConnected: false,
isDBError: false,
};
...
...
}
export default SelectorReducer;
actions.js
export const initializeDBConnection = () => {
return {
type: 'DB_CONNECT_INIT'
}
};
ParentComponent.js
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux'; //import provider to provide component access to the state
//Component imports
import SelectForm from './components/SelectForm'
import SelectorReducer from '.../reducer.js'
const SelectorStore = createStore(SelectorReducer);
const ParentComponent = () => {
return (
<div className="page-container">
<div id="carousel">
<div id="wrapper">
<Provider store={SelectorStore}>
<SelectForm />
</Provider>
</div>
</div>
</div>
)
}
SelectForm.js (Child Component, wrapped in Provider tags above)
//IMPORTS
import React from 'react'; //import react
import { useSelector, useDispatch } from 'react-redux';
//COMPONENT IMPORTS
import FormGroup from '../FormGroup';
import { * as actions } from '.../actions.js';
const SelectForm = (props) => {
//STATEFUL IMPORTS
//filter
const filter = useSelector(state => state.filter);
Credit to #NicholasTower for the answer in the comments. My reducer did not have a default case in which
default: return state
Putting that in solved the issue.
let filter = useSelector(state => {
console.log('State: ', state);
return state.pieChart.filter;
});
Make sure that you are using the correct object for state.
Add a console for debugging and check the state object. Many times we use multiple reducers which makes our component state nested in global object.
We should import useSelector form react-redux and then select user details form store in the following way
import { useSelector } from 'react-redux'
const User = () => {
const userInfo= useSelector(state => state.user)
return <div>{userInfo.name}</div>
}
Inside ParentComponent.js you are saying <Provider store={TeamSelectorStore}>. I think you meant to say <Provider store={SelectorStore}>
In my case I was storing the data coming from Redux state at didMount stage whereas the Redux states weren't fully uploaded yet.
const currentUser = useAuth();
useEffect(() => {
form.setFieldValue('email', currentUser.email);
}, []);
Adding currentUser to useEffect dependency array, get it resolved for me:
useEffect(() => {
form.setFieldValue('email', currentUser.email);
}, [currentUser]);
Sometimes having a break in the switch block of the reducer can cause the prop not defined in the state.
This error can also happen because of an uppercase / lowercase mistake.
let horns = useSelector(state => state.largeRhinohorns);
let horns = useSelector(state => state.largeRhinoHorns);
Now I'm trying to use useReducer to created a new way for management state and function but now found the problem is "Hooks can only be called inside of the body of a function component"
Is there any way to solve this problem?
// App Component
import React from "react";
import { product, productDis } from "./ProductReducer";
//{product} is state, {productDis} is dispatch
import { total } from "./TotalReducer";
//{total} is state and i dont need {totalDis}
const App = () => {
return (
<div>
<button onClick={()=>productDis({type:'add',payload:'pen'})}>add</button>
{product} {total}
</div>
);
};
export default App;
// ProductReducer Component
import React, { useReducer } from 'react';
import {totalDis} from './TotalReducer'
//{totalDis} is dispatch and i dont need {total}
export const [product, productDis] = useReducer((state, action) => {
switch (action.type) {
case "add": {
const product_0 = 'pencil'
const product_1 = `${action.payload} and ${product_0}`
totalDis({
type:'total_add',
payload:'250'
})
return product_1;
}
default:
return state;
}
}, []);
// TotalReducer Component
import React, { useReducer } from 'react';
export const [total, totalDis] = useReducer((total, action) => {
switch (action.type) {
case "total_add": {
const vat = action.payload*1.15
return vat;
}
default:
return total;
}
}, 0)
when i click the button on display It should be shown..." pen and pencil 287.5 "
but it show "Hooks can only be called inside of the body of a function component"
there any way to solve this problem? or i should back to nature?
React hooks should be called only inside functional components. Hook state is maintained per component instance. If hooks have to be reused, they can be extracted into custom hooks, which are functions that call built-in hooks and are supposed to be called inside functional components:
export const useTotal = () => {
const [total, totalDis] = useReducer((total, action) => {...}, 0);
...
return [total, totalDis];
};
In case there's a need to maintain common state for multiple components it should be maintained in common parent and be provided to children through props:
const Root = () => (
const [total, totalDispatcher] = useTotal();
return <App {...{total, totalDispatcher}}/>
);
const App = props => {
return (
<div>{props.total}</div>
);
};
Or context API:
const TotalContext = createContext();
const Root = () => (
<TotalContext.Provider value={useTotal()}>
<App/>
</TotalContext.Provider>
);
const App = () => {
const [total] = useContext(TotalContext);
return (
<div>{total}</div>
);
};
With useEnhancedReducer hook introduced here which returns getState function.
You will have something like.
const [state, dispatch, getState] = useEnahancedReducer(reducer, initState)
Because dispatch, getState will never change, they can be used in some hooks without their appearance in the dependence list, they can be stored somewhere else (outside of react) to to be called at anytime, from anywhere.
There is also version of useEnhancedReducer which supports adding middleware, in the same article.
From the docs,
There are three common reasons you might be seeing it:
You might have mismatching versions of React and React DOM.
You might be breaking the Rules of Hooks.
You might have more than one copy of React in the same app.
Deep drive to the docs. I hope, you'll be able to resolve the issue. Especially see:
Breaking the Rules of Hooks:
function Counter() {
// ✅ Good: top-level in a function component
const [count, setCount] = useState(0);
// ...
}
function useWindowWidth() {
// ✅ Good: top-level in a custom Hook
const [width, setWidth] = useState(window.innerWidth);
// ...
}
If you break these rules, you might see this error.
function Bad1() {
function handleClick() {
// 🔴 Bad: inside an event handler (to fix, move it outside!)
const theme = useContext(ThemeContext);
}
// ...
}
function Bad2() {
const style = useMemo(() => {
// 🔴 Bad: inside useMemo (to fix, move it outside!)
const theme = useContext(ThemeContext);
return createStyle(theme);
});
// ...
}
class Bad3 extends React.Component {
render() {
// 🔴 Bad: inside a class component
useEffect(() => {})
// ...
}
}
To conclude, your error seems to be appearing as if you're using reducer inside click handler. Check the example Bad1 to resolve your issue. What I mean here is you shouldn't be doing like this:
onClick={()=>productDis({type:'add',payload:'pen'})}
In the onClick handler, dispatch the action and inside a method use that reducer.
Currently I'm having to reconfigure my store to create selector. Is there a better way to do this.
import { configureStore } from "../configureStore";
const { store } = configureStore();
export const getSession = () => store.getState().session.data || false;
export const getToken = () => store.getState().session.data.token || false;
Selector functions should take the store state as the argument, not capture the store reference. As an example:
export const getSession = (state) => state.session.data || false;
export const getToken = (state) => state.session.data.token || false;
// use like:
const session = getSession(store.getState());
See my post Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance for more details.
If you are going to use Redux in React world, then you should definitely attach the redux store to the props of a React component by using connect (https://redux.js.org/basics/usage-with-react).
That way, whenever the values in the store changes, you get updated props provided to your component, re-rendering the component with the correct values.
With that, you generally don't ever need store.getState(). You do something like:
// selectors.js
const getSession = state => state.session.data || false
// component.js
const MyComponent = ({ session }) => <div>{session}</div>
const mapStateToProps = state => ({
session: getSession(state),
})
export default connect(mapStateToProps)(MyComponent)
I am breaking apart Redux' todo example to try to understand it. I read that mapDispatchToProps allows you to map dispatch actions as props, so I thought of rewriting addTodo.js to use mapDispatchToProps instead of calling dispatch(addTodo()). I called it addingTodo(). Something like this:
import React from 'react';
import {connect} from 'react-redux';
import addTodo from '../actions';
let AddTodo = ({addingTodo}) => {
let input;
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
addingTodo(input.value)
input.value = ""
}}>
<input ref={node => {
input = node
}} />
<button type="submit">Submit</button>
</form>
</div>
)
}
const mapDispatchToProps = {
addingTodo: addTodo
}
AddTodo = connect(
mapDispatchToProps
)(AddTodo)
export default AddTodo
However, when I run the app, I get this error: Error: Invalid value of type object for mapStateToProps argument when connecting component AddTodo.. I never used mapStateToProps to begin with on AddTodo component, so I was not sure what was wrong. My gut feeling says that connect() expects mapStateToProps to precede mapDispatchToProps.
The working original looks like this:
import React from 'react';
import {connect} from 'react-redux';
import addTodo from '../actions';
let AddTodo = ({dispatch}) => {
let input;
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ""
}}>
<input ref={node => {
input = node
}} />
<button type="submit">Submit</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)
export default AddTodo
Complete repo can be found here.
So my question is, is it possible to do mapDispatchToProps without mapStateToProps? Is what I am trying to do an acceptable practice - if not, why not?
Yes, you can. Just pass null as first argument:
AddTodo = connect(
null,
mapDispatchToProps
)(AddTodo)
Yes, it's not just acceptable practice, it's recommended way to trigger actions. Using mapDispatchToProps allows to hide the fact of using redux inside your react components