Is this the best way to connect Redux store with hooks?
Hi I am making a simple todo app using React hooks connected to Redux. The pattern I created works but I wonder if I'm doing it right, is there something wrong with this approach, is there a different pattern one should use?
App.jsx
const [initialTodos, updateTodos] = useState(store.getState());
const cleanup = store.subscribe(() => updateTodos(store.getState()));
useEffect(() => {
return () => cleanup();
});
dispatching happens in other components + the todo app works
Thank you for any input
react-redux package supports hooks since v7.1. Redux hooks can be used instead of the connect() function.
This is an example implementing a counter where the counter value is managed by Redux.
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
export const CounterComponent = () => {
const dispatch = useDispatch();
const counter = useSelector(state => state.counter);
return (
<div>
<span>{counter}</span>
<button onClick={() => dispatch({ type: 'INCREMENT_COUNTER' })}>
Increment counter
</button>
</div>
);
}
Source: https://react-redux.js.org/api/hooks
It turns out it is better too use 'react-redux' connect.
const mapStateToProps = state => ({state: state})
connect(mapStateToProps)(Component)
since connect automagically takes care of store.subscribe etc.
So drop hooks all together :)
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)
I encountered a problem with my react application related to hooks. Technologies being used: React, Redux, Apollo, ChakraUI.
Here is the React component that is troubling me:
import React, { useEffect } from "react";
import { Flex, Container, Heading, Text } from "#chakra-ui/react";
import { connect, useSelector, useDispatch } from "react-redux";
import { State } from "../state/store";
import { fetchRecipes } from "../state/recipe/actions";
interface RecipesListProps {}
const RecipesList: React.FC<RecipesListProps> = ({}) => {
const recipes = useSelector<State>(
(state) => state.recipe.recipes
) as State["recipe"]["recipes"];
const loading = useSelector<State>(
(state) => state.recipe.loading
) as State["recipe"]["loading"];
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchRecipes());
}, []);
if (loading) {
return <h1>Loading....</h1>;
}
return (
<Flex
m="auto"
mt="5rem"
w="50%"
direction="column"
justifyContent="center"
alignItems="center"
>
<Heading>Your Recipes</Heading>
<Flex mt="2rem" direction="column" w="100%" padding="0" gridGap="2rem">
{recipes &&
recipes.map((recipe) => (
<Container
key={recipe.id}
bg="orange.100"
borderRadius="0.2rem"
padding="1rem"
maxW="100%"
>
<Text fontSize="xl" fontWeight="bold">
{recipe.title}
</Text>
<Text>{recipe.description}</Text>
</Container>
))}
</Flex>
</Flex>
);
};
export default RecipesList;
Notice the use of the useEffect() hook. This is the error I am getting:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
I am pretty sure I am disobeying rule number, 2 i.e. I am breaking the Rules of Hooks. As soon as I take the useEffect() call out of the component, it doesn't throw an error.
Could someone please give some guidance as to what I am doing wrong?
Thanks.
Edit:
The fetchRecipes function is a Redux thunk function that fetches recipes from a graphql server
Update:
I have been hacking away at a solution to this problem. I replaced the dispatch(fetchRecipes()) call with a console.log("hello world"), and it worked perfectly!
This is boggling my mind! Is this a problem with the fetchRecipes function?
Edit:
Here's the code for the fetchRecipes function:
export const fetchRecipes = () => {
return (dispatch: Dispatch) => {
dispatch(fetchRecipesPending());
const { data } = useRecipesQuery();
const errors = data?.recipes.errors;
const recipes = data?.recipes.recipes;
if (errors?.length) {
dispatch(fetchRecipesFailure(errors));
} else {
dispatch(fetchRecipesSuccess(recipes));
}
};
};
useRecipesQuery is a custom hook that was auto generated using the graphql-codegen library. It builds up on the useQuery hook from the #apollo/client library.
Your useEffect needs a little rewrite. You are dispatching the function fetchRecipes which in itself is a hook, but the thing dispatched should be a plain "action" (using Redux terminology here). So I guess we can fix that by breaking up your fetchRecipes fn.
A snippet of the component would now look like following:
const { data } = useRecipesQuery();
useEffect(() => {
if (!data) {
dispatch(fetchRecipesPending()) // I only assume you fetch on render
}
if (data?.recipes?.errors) {
dispatch(fetchRecipesFailure(data?.recipes.errors)
}
if (data?.recipes?.recipes) {
dispatch(fetchRecipesSuccess(data?.recipes?.recipes)))
}
}, [data]);
Now it should be fine AND more readable. Either way, as some has already suggested, I would think about using some more standardised way like using Redux w/ Thunks or Sagas, or, even better - I see you might be doing a GQL query - if so, just use a hook for it and handle the data with Apollo Client.
I am encountering a function not found using useDispatch. Supposedly, I should be using react-redux, unfortunately, my version is 6.0.0 and what's needed is 7.0.0 and above. I'd like to explore useDispatch or related hook to dispatch an action function inside a function component. How do I do this given that I cannot upgrade my react-redux & still use function component?
My alternative is to use class component, but again, I'd like to see this work in function component. Let me know what other details needed.
Here's my code.
import React, { useDispatch } from 'react';
import {
Tooltip,
IconButton,
Icon,
} from '#material-ui/core';
import { logout } from './auth/store/actions';
const QuickLogout = () => {
const dispatch = useDispatch();
const handleClick = () => {
dispatch(logout());
};
return (
<Tooltip title="Logout" placement="bottom">
<IconButton className="w-64 h-64" onClick={handleClick}>
<Icon>exit_to_app</Icon>
</IconButton>
</Tooltip>
);
};
export default QuickLogout;
And here's the error when compiling.
TypeError: Object(...) is not a function
QuickLogout
webpack-internal:///.....onents/QuickLogout.js:18:75
Edit: I mentioned exploring useDispatch or related hooks given that I cannot upgrade. I felt that this is a gray statement as I really just wanted my code to work. That means, solution is not limited to useDispatch or other hooks. Hence, I chose the answer that did not use useDispatch or hooks because it was plain simple. Apologies for the vague statement. I'll take note on improving my writing skills.
React itself doesn't have useDispatch in it Hooks API, but it has useReducer to eliminate redux for a small project that redux is an overengineering process for them.
On the other hand, new versions of react-redux provide new handy hooks which you can make a similar version of those by yourself.
Here is a Tip
Hooks (React hooks) can only be used in functional component
HOC (Higher-order component) can be used anywhere
The connect function which react-redux provides is HOC so it can also be used with both class-base and functional components. It is also a good practice to separate your presentation UI and logic into representationals and containers, hence there will be no problem for your component to be functional and use all fancy things that could be used in traditional class-component and connect by react-redux.
you can also make your custom hook for handling such things in a re-usable manner with less code.
// setup.js
const store = createStore(/* put your reducer and enhancers here */)
export const { dispatch, getState } = store
// hooks.js
import { dispatch, getState } from '~setup.js'
export const useDispatch = () => dispatch
export const useSelector = selector => selector(getState())
// The above useSelector wont cause your component to re-render on data change
Then in your component
import { useDispatch } from '~hooks.js'
import { logout } from './auth/store/actions'
export default function QuickLogout() {
const dispatch = useDispatch()
const handleClick = () => {
dispatch(logout())
}
return (
<Tooltip>
<IconButton onClick={handleClick}>
<Icon>exit_to_app</Icon>
</IconButton>
</Tooltip>
)
}
If you want to know more about custom hooks see making my own hook.
If you can't upgrade your react-redux version to one with the useDispatch and useSelector React hooks you can still use the connect Higher Order Component to inject dispatch, or to even more simply wrap your action creators with a call to dispatch.
import { connect } from 'react-redux';
const QuickLogout = ({ logout }) => { // <-- destructure logout prop
return (
<Tooltip title="Logout" placement="bottom">
<IconButton
className="w-64 h-64"
onClick={logout} // <-- assign to click handler
>
<Icon>exit_to_app</Icon>
</IconButton>
</Tooltip>
);
};
const mapDispatchToProps = {
logout, // <-- inject logout action as prop already wrapped by dispatch!
};
const ConnectedQuickLogout = connect(
null, // <-- this is typically mapStateToProps, but we don't need it
mapDispatchToProps,
)(QuickLogout);
export default ConnectedQuickLogout;
If you prefer to keep the code closer to as-is then you can simply connect it to the redux store and a dispatch prop is still injected.
import { connect } from 'react-redux';
const QuickLogout = ({ dispatch }) => {
const handleClick = () => {
dispatch(logout());
};
return (
<Tooltip title="Logout" placement="bottom">
<IconButton className="w-64 h-64" onClick={handleClick}>
<Icon>exit_to_app</Icon>
</IconButton>
</Tooltip>
);
};
export default connect()(QuickLogout);
I am new to React and Redux and as we know, it is best to use class component for those components that have state and the question I would like to ask is that Is it recommended to use functional component for components that have connection and interaction with Redux store since those components that interact with store do not have state locally.
As of version 7.x react-redux now has hooks for functional components
const store = useSelector(store => store)
So that we can use functional component with redux store like class component.
please check below link to get more idea about hooks
https://react-redux.js.org/next/api/hooks
It's perfectly fine to connect functional components to redux store.
Functional components don't have a state is not completely correct with hooks. You can add state to functional component with hooks.
Answering your question, you can connect functional component with redux store like below.
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider, connect } from "react-redux";
const reducers = (state = 0, action) => {
switch (action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
default:
return state;
}
};
const store = createStore(reducers, 0);
const App = ({ count, handleIncrement, handleDecrement }) => {
return (
<div>
<button onClick={handleIncrement}>+</button>
<h4>{count}</h4>
<button onClick={handleDecrement}>-</button>
</div>
);
};
const mapStateToProps = state => {
return { count: state };
};
const mapDispatchToProps = dispatch => {
return {
handleIncrement: () => {
dispatch({ type: "INCREMENT" });
},
handleDecrement: () => {
dispatch({ type: "DECREMENT" });
}
};
};
const ConnectedApp = connect(
mapStateToProps,
mapDispatchToProps
)(App);
ReactDOM.render(
<Provider store={store}>
<ConnectedApp />
</Provider>,
document.getElementById("root")
);
Is it recommended to use functional components for components that have connection and interaction with the Redux store since those components that interact with the store do not have state locally.
Yes, it is recommended to use functional components with redux, and there is a way to have a local state in a functional component.
Why functional components are recommended?
The react ecosystem moves toward the use of hooks which means standardize the functional components.
As stated in docs about uses of hooks or classes:
In the longer term, we expect Hooks to be the primary way people write React components.
How to have a local state in functional components with redux?
Redux introduced redux-hooks API which gives functional components the ability to use local component state and allows to subscribe to the Redux store without wrapping your components with connect().
useSelector
useDispatch
useStore
// Creating a store
const store = createStore(rootReducer)
ReactDOM.render(
<Provider store={store}>
<CounterComponent />
</Provider>,
document.getElementById('root')
)
// CounterComponent.jsx Selector example
import React from 'react'
import { useSelector } from 'react-redux'
export const CounterComponent = () => {
// Using the store localy with a selector.
const counter = useSelector(state => state.counter)
return <div>{counter}</div>
}
// CounterComponent.jsx Dispatch Example
import React from 'react'
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) => {
// Dispatching an action
const dispatch = useDispatch()
return (
<div>
<span>{value}</span>
<button onClick={() => dispatch({ type: 'increment-counter' })}>
Increment counter
</button>
</div>
)
}
// CounterComponent.jsx Referencing the store example
import React from 'react'
import { useStore } from 'react-redux'
export const CounterComponent = ({ value }) => {
const store = useStore()
// EXAMPLE ONLY! Do not do this in a real app.
// The component will not automatically update if the store state changes
return <div>{store.getState()}</div>
}
I have problems with using Redux saga with react hooks. It clearly states that useSelector() will subscribe to the Redux store, and run the selector whenever an action is dispatched.
here is the component im trying to update:
import React from 'react';
import { useSelector} from "react-redux";
const InfoPage = () => {
const summoner = useSelector(state => state.summoner);
const loading = useSelector(state => state.loading);
console.log(loading,summoner) {*/Both undefined and only gets called once*/}
if(!summoner){
return <div>Loading...</div>
}
return(
<div>
{summoner.name}
</div>
)
}
export default InfoPage;
The component gets called via a history.push('/infopage') when I dispatch the action to fetch the "summoner", which again dispatches another action when successfully managing to fetch summoner. This should rerender my InfoPage if i understand this right.
My redux store has the values after the fetch, but the InfoPage won't rerender, heres the last state:
heres also my reducer
I found the problem... I've forgot that my saga reducer is called "saga" when exporting it. so to fix this problem i had to change:
const summoner = useSelector(state => state.summoner);
to
const summoner = useSelector(state => state.saga.summoner);