I'm attempting to create a React/Redux component that shows/hides an element when clicked.
I'm using this to trigger the function from another component:
import React from 'react'
//Some other code...
import { useSelector } from 'react-redux'
import onShowHelpClicked from '../help/AddHelpSelector'
<button onClick={onShowHelpClicked}>Help</button>
This this is AddHelpSelector:
import { useState } from 'react'
import { useDispatch } from 'react-redux'
import { helpVisible } from './HelpSlice'
export const AddHelp = () => {
const [isVisible, showHelp] = useState('')
const dispatch = useDispatch()
const onShowHelpClicked = () => {
dispatch(
helpVisible({
isVisible,
})
)
if (isVisible) {
showHelp(false)
} else {
showHelp(true)
}
}
return (
<section>
<h2 style={{ visibility: { isVisible } }}>Help section</h2>
</section>
)
}
export default AddHelp
Finally, this is HelpSlice
import { createSlice } from '#reduxjs/toolkit'
const initialState = [{ isVisible: false }]
const helpSlice = createSlice({
name: 'help',
initialState,
reducers: {
helpVisible(state, action) {
state.push(action.payload)
},
},
})
export const { helpVisible } = helpSlice.actions
export default helpSlice.reducer
I'm fairly certain I'm doing multiple things wrong, as this is my first attempt to do anything with Redux and I'm still struggling to wrap my mind around it after a week of learning.
But specifically, when clicking the help button I get this error.
"Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app"
The linked documentation provides a way to test a component to see if React is importing properly, and it's not. But I'm not really sure what I'm doing wrong.
I think it may be that I'm importing React multiple times, but if I don't then I can't use "useState."
What's the correct way to do this? I'm open to corrections on both my code as well as naming conventions. I'm using boilerplate code as a template as I try to understand this better after getting through the documentation as well as Mosh's 6 hour course which I just finished.
You're importing the < AddHelpSelector /> component here import onShowHelpClicked from '../help/AddHelpSelector', and then you try to use it as a callback handler for the button's onClick, which doesn't really make sense. I assume you actually wanted to only import the onShowHelpClicked function declared inside the < AddHelpSelector /> component (which is not really a valid way of doing it). Since you want to control the visibility using redux state, you could just grab the flag from the redux store inside the < AddHelpSelector /> component using useSelector hook. To set it, you're gonna do that in the component where your button is. For that, you just need to dispatch an action(like you already did), with the updated flag. No need for the local useState. Also, using the flag you could just conditionally render the element.
const App = () => {
const dispatch = useDispatch();
const { isVisible } = useSelector((state) => ({ isVisible: state.isVisible }));
const handleClick = () => {
dispatch(
helpVisible({
!isVisible,
})
)
}
return (<button onClick={handleClick}>Help</button>);
}
export const AddHelp = () => {
const { isVisible } = useSelector((state) => ({ isVisible: state.isVisible }));
return (
<section>
{isVisible && <h2>Help section</h2>}
</section>
)
}
export default AddHelp
Related
Store.js:
import {useReducer} from "react";
import {ACTION_TYPES} from "./storeActionTypes";
export const INITIAL_STATE = {
counter: 0,
};
export const storeReducer = (state, action) => {
switch (action.type) {
case ACTION_TYPES.INCREASE_COUNTER_BY_1:
return {
...state,
counter: state.counter + 1,
};
default:
return state;
}
};
const Store = () => {
const [state, dispatch] = useReducer(storeReducer, INITIAL_STATE);
return [state, dispatch];
};
export default Store;
AnyComponent.js
import React from "react";
import Store from "../store/Store";
const AnyComponent = () => {
const [store, dispatch] = Store();
const handleInceaseByOne = (e) => {
e.preventDefault();
dispatch({type: "INCREASE_COUNTER_BY_1"});
};
return (
<div>
<button onClick={(e) => handleInceaseByOne(e)}>Submit</button>
<span>counter from AnyComponent.js:{store.counter}</span>
</div>
);
};
export default AnyComponent;
OtherComponent.js
import React from "react";
import Store from "../store/Store";
const OtherComponent.js = () => {
const [store, dispatch] = Store();
return (
<div>
<span>counter from OtherComponent.js:{store.counter}</span>
</div>
);
};
export default OtherComponent.js;
So basically like in Redux, create a one Store where you store everything. In AnyComponent.js we have button who increase counter by 1 so we can see that value of store.counter in AnyComponent.js and OtherComponent.js.
Please anyone tell me if anything is wrong with this code?
Will try to upload this to GitHub later.
I looked in web and did not found anything what is similar to this so please let me know what you think.
If you actually try this one, you will see that the counter state of one component does not reflect on the second one. You haven't created a single state but 2 separate ones.
Since you call Store() in each component which leads to a new call to useReducer, you create a new instance of this reducer/state which is standalone and can only be used from the component where it's called (or it's children if passed down as props).
What you've done here is to create your own custom hook and this is used for re-usability only and not shared state. Shared state can be achieved through a lot of other alternatives (such as react context).
Feel free to check this out in this reproducable codesandbox.
From the React Docs:
"Only call Hooks from React function components. Don’t call Hooks from regular JavaScript functions. (There is just one other valid place to call Hooks — your own custom Hooks. [...]." (https://reactjs.org/docs/hooks-overview.html)
WHILE WRITING THIS POST I REALIZED WHAT THE SOLUTION WAS
Every time I dispatch a task to my store the following error occurs:
I have some idea of why it happens. It happens precisely when I try to get the to-do list using useSelector and then mapping through the list. However, the mapping is not the issue but rather returning a react component on the map function. It works just fine if I do not return a functional component and instead use HTML. So the issue, from my POV, is returning a react functional component while passing props to it on a map function.
Here's the code for my home component:
import Input from '../components/Input';
import TodoForm from '../components/TodoForm';
function Home() {
document.title = "MyTodo | Home"
return (
<div className="App">
<h1>MyTodo</h1>
<Input />
<TodoForm />
</div>
);
}
export default Home;
The input component where the action is being dispatched on key down:
import {useState} from 'react'
import { useDispatch } from 'react-redux';
import { todoActions } from '../store/todo';
const Input = () => {
const [inputText, setInputText] = useState("");
const dispatch = useDispatch();
const handleChange = (e) => setInputText(e.target.value)
const handleKeyPress = (event) => {
if (event.code === "Enter") {
// if the expression is false, that means the string has a length of 0 after stripping white spaces
const onlyWhiteSpaces = !inputText.replace(/\s/g, "").length;
!onlyWhiteSpaces &&
dispatch(
todoActions.addTask({ label: inputText, done: false })
);
setInputText("");
}
};
return (
<input
type="text"
onKeyDown={(e) => handleKeyPress(e)}
onChange={(e) => handleChange(e)}
value={inputText}
/>
);
}
export default Input
The TodoForm where I am using useSelector to get the todo list from the redux store and mapping thru it:
import { useSelector } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import TodoTask from "./TodoTask";
const TodoForm = () => {
const tasks = useSelector((state) => state.todo.taskList);
const renderedListItems = tasks.map((task, index) => {
return (
<TodoTask
key={uuidv4()}
task={task}
targetIndex={index}
/>
);
});
return <div className="container">{renderedListItems}</div>;
};
export default TodoForm;
Finally the TodoTask component which is the child component being returned on the map function above:
import { useDispatch } from "react-redux";
import { todoActions } from "../store/todo";
const TodoTask = ({ task, targetIndex }) => {
const {text, done} = task;
console.log("Task: ", task);
const dispatch = useDispatch()
const removeTask = dispatch(todoActions.deleteTask(targetIndex))
return (
<div
className="alert alert-primary d-flex justify-content-between"
role="alert"
>
{text}
<button type="button" className="btn-close" onClick={()=>removeTask}></button>
</div>
);
};
export default TodoTask;
This is my first time facing this issue, and I know it has something to do with redux and how the useSelector hook forces a component to re-render. So the useSelector is re-rendering the TodoForm component, and since we are mapping and returning another component, that component is also being rendered simultaneously. At least, that is how I understand it. Let me know if I am wrong.
Things I have tried:
Wrapping the TodoTask in React.memo. Saw it somewhere as a possible solution to this kind of issue, but that did not work.
Passing shallowEqual as a second parameter on the TodoForm useSelector. This does prevent the page from going into an infinity loop, but the tasks show up empty but are being added to the redux store. However, with this method, the first warning stills shows up, and the console log in the TodoTask component does not execute.
Passing shallowEqual as a second parameter on the TodoForm useSelector. This does prevent the page from going into an infinity loop but the tasks show up empty but are being added to the redux store. However, with this method, the first warning stills shows up and the console log in the TodoTask component does not execute.
I realized what I was doing wrong while writing this part. The console log in the TodoTask component was working, but I had the browser console filtering for errors only. When I check the messages section, I saw everything working fine. Then when I checked the Task component, I noticed I was trying to read a property that did not exist and hence why the tasks had no text.
In other words, the solution was adding shallowEqual as second parameter of the useSelector hook in my TodoForm component that was the one mapping thru the todo tasks array. As I said, useSelector forces a component to re-render. shallowEquals checks if the existing state isn't the same as we already had and avoids unnecessary re-renders, which can lead my application to exceed the maximum update length.
Code fix [Solution]:
import { memo } from "react";
import { shallowEqual, useSelector } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import TodoTask from "./TodoTask";
const TodoForm = () => {
// shallowEqual prevents unnecessary re-renders which can lead to an infinite loop
// it compares the current state with the previous one, if they are the same, it does not re-render the component
const tasks = useSelector((state) => state.todo.taskList, shallowEqual);
const renderedListItems = tasks.map((task, index) => {
return (
<TodoTask
key={uuidv4()}
task={task}
targetIndex={index}
/>
);
});
return <div className="container">{renderedListItems}</div>;
};
export default memo(TodoForm);
Honestly, I have been stuck on this since yesterday and I cannot believe I realize the solution just when I was about to ask for help. Hope this helps anyone else who faces a similar issue in the future.
I'm trying to make a permissions provider that wraps some react-redux global state. I have as follows:
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
export const PermissionsContext = React.createContext();
const getUserAbilities = createSelector(
state => state.user.abilities,
abilities =>
abilities.reduce((acc, val) => {
acc[val] = true;
return acc;
}, {})
);
function useAbilities() {
const abilities = useSelector(getUserAbilities);
return { abilities };
}
export const PermissionsProvider = ({ children }) => {
const { abilities } = useAbilities();
const can = useCallback((...permissions) => permissions.every(permission => permission in abilities), [
abilities
]);
return <PermissionsContext.Provider value={{ can }}>{children}</PermissionsContext.Provider>;
};
export const withPermissions = WrappedComponent => {
return class ComponentWithPermissions extends React.Component {
render() {
return (
<PermissionsContext.Consumer>
{props => <WrappedComponent {...this.props} permissions={props} />}
</PermissionsContext.Consumer>
);
}
};
};
Usage of PermissionsProvider:
<PermissionsProvider>
<App />
</PermissionsProvider>
This includes a context so I can useContext(PermissionsContext) and also a HOC withPermissions so that I wrap legacy class components with it.
In the case of a class, I would call this.props.permissions.can('doThing1', 'doThing2') and it should return true or false depending on whether all of those abilities are present in the user payload.
It seems to be functioning fine except when I try to commit it, I get the error:
React Hook "useSelector" is called in function "can" which is neither a React function component or a custom React Hook function react-hooks/rules-of-hooks
I saw a few issues with naming convention, but that doesn't seem to apply here(?). I also used to have the useAbilities hook inside the function just above the can function which also threw the error.
Any ideas?
All the above code was correct. It seemed to be some sort of eslint cache that was continuing to complain. After clearing cache and rerunning lint it went away.
It is my first application using react context with hooks instead of react-redux and would like to get help of the structure of the application.
(I'm NOT using react-redux or redux-saga libraries.)
Context
const AppContext = createContext({
client,
user,
});
One of actions example
export const userActions = (state, dispatch) => {
function getUsers() {
dispatch({ type: types.GET_USERS });
axios
.get("api address")
.then(function(response) {
dispatch({ type: types.GOT_USERS, payload: response.data });
})
.catch(function(error) {
// handle error
});
}
return {
getUsers,
};
};
Reducer (index.js): I used combineReducer function code from the redux library
const AppReducer = combineReducers({
client: clientReducer,
user: userReducer,
});
Root.js
import React, { useContext, useReducer } from "react";
import AppContext from "./context";
import AppReducer from "./reducers";
import { clientActions } from "./actions/clientActions";
import { userActions } from "./actions/userActions";
import App from "./App";
const Root = () => {
const initialState = useContext(AppContext);
const [state, dispatch] = useReducer(AppReducer, initialState);
const clientDispatch = clientActions(state, dispatch);
const userDispatch = userActions(state, dispatch);
return (
<AppContext.Provider
value={{
clientState: state.client,
userState: state.user,
clientDispatch,
userDispatch,
}}
>
<App />
</AppContext.Provider>
);
};
export default Root;
So, whenever the component wants to access the context store or dispatch an action, this is how I do from the component :
import React, { useContext } from "react";
import ListMenu from "../common/ListMenu";
import List from "./List";
import AppContext from "../../context";
import Frame from "../common/Frame";
const Example = props => {
const { match, history } = props;
const { userState, userDispatch } = useContext(AppContext);
// Push to user detail route /user/userId
const selectUserList = userId => {
history.push(`/user/${userId}`);
userDispatch.clearTabValue(true);
};
return (
<Frame>
<ListMenu
dataList={userState.users}
selectDataList={selectUserList}
/>
<List />
</Frame>
);
};
export default Example;
The problem I faced now is that whenever I dispatch an action or try to access to the context store, the all components are re-rendered since the context provider is wrapping entire app.
I was wondering how to fix this entire re-rendering issue (if it is possible to still use my action/reducer folder structure).
Also, I'm fetching data from the action, but I would like to separate this from the action file as well like how we do on redux-saga structure. I was wondering if anybody know how to separate this without using redux/redux-saga.
Thanks and please let me know if you need any code/file to check.
I once had this re-rendering issue and I found this info on the official website:
https://reactjs.org/docs/context.html#caveats
May it will help you too
This effect (updating components on context update) is described in official documentation.
A component calling useContext will always re-render when the context value changes. If re-rendering the component is expensive, you can optimize it by using memoization
Possible solutions to this also described
I see universal solution is to useMemo
For example
const Example = props => {
const { match, history } = props;
const { userState, userDispatch } = useContext(AppContext);
// Push to user detail route /user/userId
const selectUserList = userId => {
history.push(`/user/${userId}`);
userDispatch.clearTabValue(true);
};
const users = userState.users;
return useMemo(() => {
return <Frame>
<ListMenu
dataList={users}
selectDataList={selectUserList}
/>
<List />
</Frame>
}, [users, selectUserList]); // Here are variables that component depends on
};
I also may recommend you to completly switch to Redux. You're almost there with using combineReducers and dispatch. React-redux now exposes useDispatch and useSelector hooks, so you can make your code very close to what you're doing now (replace useContext with useSelector and useReducer with useDispatch. It will require minor changes to arguments)
Is there a way with new react hooks API to replace a context data fetch?
If you need to load user profile and use it almost everywhere, first you create context and export it:
export const ProfileContext = React.createContext()
Then you import in top component, load data and use provider, like this:
import { ProfileContext } from 'src/shared/ProfileContext'
<ProfileContext.Provider
value={{ profile: profile, reloadProfile: reloadProfile }}
>
<Site />
</ProfileContext.Provider>
Then in some other components you import profile data like this:
import { ProfileContext } from 'src/shared/ProfileContext'
const context = useContext(profile);
But there is a way to export some function with hooks that will have state and share profile with any component that want to get data?
React provides a useContext hook to make use of Context, which has a signature like
const context = useContext(Context);
useContext accepts a context object (the value returned from
React.createContext) and returns the current context value, as given
by the nearest context provider for the given context.
When the provider updates, this Hook will trigger a rerender with the
latest context value.
You can make use of it in your component like
import { ProfileContext } from 'src/shared/ProfileContext'
const Site = () => {
const context = useContext(ProfileContext);
// make use of context values here
}
However if you want to make use of the same context in every component and don't want to import the ProfileContext everywhere you could simply write a custom hook like
import { ProfileContext } from 'src/shared/ProfileContext'
const useProfileContext = () => {
const context = useContext(ProfileContext);
return context;
}
and use it in the components like
const Site = () => {
const context = useProfileContext();
}
However as far a creating a hook which shares data among different component is concerned, Hooks have an instance of the data for them self and don'tshare it unless you make use of Context;
updated:
My previous answer was - You can use custom-hooks with useState for that purpose, but it was wrong because of this fact:
Do two components using the same Hook share state? No. Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.
The right answer how to do it with useContext() provided #ShubhamKhatri
Now i use it like this.
Contexts.js - all context export from one place
export { ClickEventContextProvider,ClickEventContext} from '../contexts/ClickEventContext'
export { PopupContextProvider, PopupContext } from '../contexts/PopupContext'
export { ThemeContextProvider, ThemeContext } from '../contexts/ThemeContext'
export { ProfileContextProvider, ProfileContext } from '../contexts/ProfileContext'
export { WindowSizeContextProvider, WindowSizeContext } from '../contexts/WindowSizeContext'
ClickEventContext.js - one of context examples:
import React, { useState, useEffect } from 'react'
export const ClickEventContext = React.createContext(null)
export const ClickEventContextProvider = props => {
const [clickEvent, clickEventSet] = useState(false)
const handleClick = e => clickEventSet(e)
useEffect(() => {
window.addEventListener('click', handleClick)
return () => {
window.removeEventListener('click', handleClick)
}
}, [])
return (
<ClickEventContext.Provider value={{ clickEvent }}>
{props.children}
</ClickEventContext.Provider>
)
}
import and use:
import React, { useContext, useEffect } from 'react'
import { ClickEventContext } from 'shared/Contexts'
export function Modal({ show, children }) {
const { clickEvent } = useContext(ClickEventContext)
useEffect(() => {
console.log(clickEvent.target)
}, [clickEvent])
return <DivModal show={show}>{children}</DivModal>
}