Today I started to build a Next.js application.
I am using Redux (next-redux-wrapper) to manage my global state.
(I am also persisting some reducer with redux-persist).
I just implemented redux-thunk and immediately got a very strange error. I really do not know why that error is occurring. I basically just did the setup, without creating any reducers (The error was still there, after I created the reducer).
It is not the first time, I did the same setup without any problems, but that time I can not get rid of that error.
Error code
Warning: Using UNSAFE_componentWillReceiveProps in strict mode is not
recommended and may indicate bugs in your code. See
https://reactjs.org/link/unsafe-component-lifecycles for details.***
Move data fetching code or side effects to componentDidUpdate.
If you're updating state whenever props change, refactor your code to use memoization ***techniques or move it to static
getDerivedStateFromProps. Learn more at:
https://reactjs.org/link/derived-state Please update the following
components: withRedux(MyApp)
If you need more code, please ask. I just do not really have much more. Maybe package.json.
_app.js
import App from 'next/app'
import { wrapper } from '../reduxStore/store'
import { useStore } from 'react-redux'
import PropTypes from 'prop-types'
function MyApp({ Component, pageProps }) {
const store = useStore((state) => state)
return <Component {...pageProps} />
}
MyApp.propTypes = {
Component: PropTypes.func,
pageProps: PropTypes.object,
}
export default wrapper.withRedux(MyApp)
store.js
import { createStore, applyMiddleware, combineReducers } from 'redux'
import { HYDRATE, createWrapper } from 'next-redux-wrapper'
import thunkMiddleware from 'redux-thunk'
import userReducer from './reducers/userReducer'
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension')
return composeWithDevTools(applyMiddleware(...middleware))
}
return applyMiddleware(...middleware)
}
const combinedReducer = combineReducers({
user: userReducer,
})
const reducer = (state, action) => {
if (action.type === HYDRATE) {
const nextState = {
...state, // use previous state
...action.payload, // apply delta from hydration
}
// if (state.count.count) nextState.count.count = state.count.count // preserve count value on client side navigation
return nextState
} else {
return combinedReducer(state, action)
}
}
export const makeStore = ({ isServer }) => {
if (isServer) {
//If it's on server side, create a store
return createStore(reducer, bindMiddleware([thunkMiddleware]))
} else {
//If it's on client side, create a store which will persist
const { persistStore, persistReducer } = require('redux-persist')
const storage = require('redux-persist/lib/storage').default
const persistConfig = {
key: 'nextjs',
whitelist: ['user'], // only counter will be persisted, add other reducers if needed
storage, // if needed, use a safer storage
}
const persistedReducer = persistReducer(persistConfig, reducer) // Create a new reducer with our existing reducer
const store = createStore(
persistedReducer,
bindMiddleware([thunkMiddleware]),
) // Creating the store again
store.__persistor = persistStore(store) // This creates a persistor object & push that persisted object to .__persistor, so that we can avail the persistability feature
return store
}
}
export const wrapper = createWrapper(makeStore)
This is because the third party library is using componentWillReceiveProps - componentWillReceiveProps gets automatically renamed to UNSAFE_componentWillReceiveProps. This happens because those methods are now considered legacy code and React is deprecating that lifecycle method as well as others.
Unfortunately, their isn't an immediate solution.
You can fork the code and update it yourself
Start an issue on the code's GIT page and hope the update their maintainer fixes the issue.
Find another library to do the same job.
Write custom logic to do the same thing as the library
Use the library and hope they fix it before it's deprecated by React.
the error caused by the third-party library (next-redux-wrapper) about Legacy Lifecycle Methods (UNSAFE), the previous name is componentWillReceiveProps, That name will continue to work until version 17.
to fix it
you can use the react-codemod to automatically update your components. and use rename-unsafe-lifecycles property
npx react-codemod rename-unsafe-lifecycles <path>
my path to a third party library is
path=./node_module/next-redux-wrapper/lib/index.js
thank you
Related
Playing around with react-redux and my state isCartVisible is showing undefined, I used simple functional components and I'm storing my stores in different files.
//main index.js file
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import store from './redux-store/store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Provider store={store}><App /></Provider>);
and
//App.js
import { useSelector } from "react-redux";
import Layout from "./components/Layout/Layout";
import Cart from "./components/Cart/Cart";
function App() {
const cartVisible = useSelector((state) => state.isCartVisible);
return (
<Layout>
{cartVisible && <Cart />}
</Layout>
);
}
and a component deep somewhere inside the app, by clicking the button I wanna toggle my <Cart> component
//CartButton.js
import { useDispatch } from "react-redux";
const CartButton = () => {
const dispatch = useDispatch();
const cartShowHandler = () => {
dispatch({ type: "cartToggle" });
};
return (
<button onClick={cartShowHandler}>
Click
</button>
);
};
and that's my store file, where I've created my store with reducer
import { createStore } from "redux";
const uiReducer = (state = { isCartVisible: true }, action) => {
if (action.type === "cartToggle") {
state.isCartVisible = !state.isCartVisible;
}
return state;
};
const uiStore = createStore(uiReducer);
export default uiStore;
You should never mutate the state. Your condition in reducer should look like this and it will work.
if (action.type === "cartToggle") {
return { ...state, isCartVisible: !state.isCartVisible};
}
As you have only one key in store in your example at the moment. You can do it this way also.
return { isCartVisible: !state.isCartVisible};
But it's always a good practice to return the whole state in your reducer's conditions.
Remember that redux do shallow comparison. Which means it checks if reference of an object is changed. In your case it wasnt changed.
Once, I wrote something about this topic in a blog post https://dev.to/machy44/shallow-comparison-in-redux-3a6
In Redux, you can only have one store. So it is very likely that your useSelector call actually tries to select data from another store than you are expecting it here.
You could validate that by using something like
const fullState = useSelector(state => state)
console.log(fullState)
That said, you are also writing a style of Redux here that is outdated by many years - in modern Redux you are not writing switch..case reducers or string action types. Also, createStore is deprecated at this point in favor of configureStore.
I would highly recommend you read about modern Redux and then follow the official Redux tutorial.
Whatever resources you have been following right now might have given you a very skewed view of how to use Redux.
I've been battling this all day long and I'd appreciate any help.
I have a redux store built with Redux Toolkit and createSlice that looks like so:
const initialState = {
analiticaNumber: "",
animal: {},
tests: [{ key: "G9116", value: "DERMATOFITOS PCR/ MUESTRA" }],
};
const PeticionSlice = createSlice({
name: "peticion",
initialState,
reducers: {
addTest: (state, action) => {
state.tests.push(action.payload);
},
},
});
export const { addTest: addTestActionCreator } = PeticionSlice.actions;
export const testsArray = (state) => state.Peticion.tests;
export default PeticionSlice.reducer;
I also have a root reducer that imports the rest of the slices and names them as such
import { combineReducers } from "redux";
import NavigationSlice from "./NavigationSlice";
const RootReducer = combineReducers({
Peticion: PeticionSlice,
});
export default RootReducer;
When I add tests to the tests array it works fine and shows in the redux devtools.
The promblem comes that react does not see the change in the store and won't update the child component:
import { testsArray } from "./Store/PeticionSlice";
That's how I import namely the testsArray to call with the useSelector.
The tradicional way of const { tests } = useSelector( (state) => state.Peticion) doesn't work either.
function App() {
const tests = useSelector(testsArray);
useEffect(() => {
console.log("tests");
}, [tests]);
return (
<StylesProvider injectFirst>
<div className="App">
<nav>
<Navbar />
</nav>
{tests.map((test) => (
<p>{test.key}</p>
))}
</div>
</StylesProvider>
);
}
I belive it has to do something with the mutability of the state, but I thought the toolkit took care of that, and for the life of me I don't know how to solve this.
Any help??? Thanks a lot.
** UPDATE **
I believe it has to do with the way I dispatch the actions. Because I needed to add several boundaries to what the app does, I decided to have an external function that filters and dispatches accordingly. It is not a react component.
import { configureStore } from "#reduxjs/toolkit";
import { addTestToList, addTestActionCreator } from "../Store/PeticionSlice";
import RootReducer from "../Store/RootReuder";
const PruebasToSubmitArray = [];
const store = configureStore({
reducer: RootReducer,
});
const handleTestList = (test, action) => {
const anatomia = "A";
const microbiologia = "M";
function oneBiopsia() {
while (test.key.toString().charAt(0) === anatomia) {
return PruebasToSubmitArray.some(
(pruebaInArray) => pruebaInArray.key.toString().charAt(0) === anatomia
);
}
return false;
}
if (!oneBiopsia() && action === "add") {
switch (test.key.toString().charAt(0)) {
case anatomia:
// console.log("Open pdf for anatomia");
store.dispatch(addTestActionCreator(test));
break;
case microbiologia:
// console.log("Open pdf for micro");
store.dispatch(addTestActionCreator(test));
break;
default:
// console.log("add test to the list, ", test);
store.dispatch(addTestActionCreator(test));
break;
}
} else if (action === "remove") {
// console.log("remove test from the list, ", test);
} else if (oneBiopsia()) {
console.log("Only one biopsia per peticion, ", newState);
}
return null;
};
export default handleTestList;
I added a button on App component and it worked as expected (i showed the updated state), as is right now redux updates the state but the component won't reflect it.
Code SandBox as complete as I can
Very odd behavior in my case.
I did
state = action.payload
and that didn't work.
Once I switched to
state.viewer = action.payload.viewer
everything worked!
Multiple Instances of Store
You create a store variable in your index.js file and pass that store to the react-redux Provider component. This is the store instance which all react-redux useSelector and useDispatch hooks will interact with.
In your HandleTestList.js file you create a different store variable. You then dispatch actions to that store, but those actions won't be reflected in your React app because this isn't the store that your React app uses.
handleTestList needs to either A) import the same global store variable. You will want to move this out of index.js and into store.js to avoid circular dependencies. or B) accept dispatch as an argument.
I am beginning with Redux and I always used it in components with connect() and mapStateToProps(), but now I want to call my API with setInterval() every x time to check if the server has new data not stored in the Redux store, and substitute it.
My approach was to create a function that reads the store and update it like that:
import { store } from './dir/dir/store.js'
const refresher = async () => {
const state = store.getState();
// Call API, compare 'state.calendar' with calendar from server
// Call store.dispatch() if they are different, then update Redux store
}
export default refresher
My questions are:
Is this a good practise to use Redux?
Is there a better approach to this problem?
Thanks
It's perfectly fine to export the store and use within a vanilla js/ts file.
Example Redux Store
Make sure to export the store that you create
import { configureStore } from "#reduxjs/toolkit";
import { slice } from "../features/counterSlice";
export const store = configureStore({
reducer: {
counter: slice.reducer
}
});
Usage in Non-Component Code:
You can then import the created store in any other code
import { store } from "../App/store";
import { slice as counterSlice } from "../features/counterSlice";
export function getCount(): number {
const state = store.getState();
return state.counter.value;
}
export function incrementCount() {
store.dispatch(counterSlice.actions.increment());
}
Traditional Usage in Functional Component
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../App/store";
import { slice as counterSlice } from "../features/counterSlice";
export function Clicker() {
const dispatch = useDispatch();
const count = useSelector((state: RootState) => state.counter.value);
const dispatchIncrement = () => dispatch(counterSlice.actions.increment())
// ...
Example Slice
import { createSlice } from "#reduxjs/toolkit";
export const slice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1;
}
}
});
Demo in Codesandbox
Note: You cannot use this option with Server Side Rendering. If you need to support SSR, you can use middleware to listen to dispatched actions and handle elsewhere.
Further Reading
What is the best way to access redux store outside a react component? | Stack Overflow
Access the Redux Store Outside a React Component | Blog
How can I access the store in non react components? | Github Issues
Here you can access the store and action out side any component like index.js file in react-native.
import {updateLocationAlertItem} from './src/store/actions/locationAlertAction';
import {store} from './src/store/index';
store.subscribe(listener);
function listener() {
state = store.getState().locationAlertReducer;
}
store.dispatch(
updateLocationAlertItem({
index: index,
is_in_radius: true,
is_notification: true,
arrival_time: moment().format('DD/MM/YYYY hh:mm'),
exit_time: item.exit_time,
}),
);
What I Just want to fetch data from api and show it at frontend. I am using Redux to call the api using it's ACTIONS and REDUCERS. In Reducers i take the intialstate as empty array.When API is successfully called, I am updating store state.Below is the practical which can help to understand concept easily.
store.js
import { createStore } from 'redux';
import reducer from './reducers/reducer';
let store = createStore(reducer)
export default store
actions.js
import {
FETCH_IMAGES_SUCCESS
} from './actionTypes'
export function fetchImages() {
return dispatch => {
return fetch("https://api.com/data")
.then(res => res.json())
.then(json => {
dispatch(fetchImagesSuccess(json.posts));
return json.posts;
})
};
}
export const fetchImagesSuccess = images => ({
type: FETCH_IMAGES_SUCCESS,
payload: { images }
});
reducer.js
import {
FETCH_IMAGES_SUCCESS
} from '../actions/actionTypes'
const initialState = {
images:[]
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_IMAGES_SUCCESS:
return {...state,images:action.payload.images}
default:
return state
}
}
export default reducer;
Now, Please tell me what should i need to do to call that Redux action and
get Data from the API.I am using React to display data.
Thanks.
In React redux usage page you can use functions like mapStateToProps and connect to do that
You need a middleware like Redux-Saga or Redux-Thunk to talk with the actions and the global store maintained using Redux.
You may follow this Tutorial: https://redux.js.org/basics/exampletodolist
If you are going with Redux-Thunk, you need to modify your store assign like this:
const store = createStore(rootReducer, applyMiddleware(thunk));
Now, have a container to all the Parent component you have.
import { connect } from 'react-redux';
import App from '../components/App';
export function mapStateToProps(appState) {
return {
/* this is where you get your store data through the reducer returned
state */
};
}
export function mapDispatchToProps(dispatch) {
return {
// make all your action dispatches here
// for ex: getData(payload) => dispatch({type: GETDATA, payload: payload})
};
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
As Mustafa said you need to use mapStateToProps. Let me explain myself.
What you just done is just the configuration for the main store (there's only one in redux). Now you need to use it in your components, but how ? When you create a Component the content of the store will be passed as props with the help of Containers.
Containers are the way to link your store with your react component.
Said that, you need to install redux and react-redux. In your code above you have successfully configured the store with the reducers with redux library. Now you need react-redux to create the Container (which wraps your react component).
Here is an example of how to put this all together:
https://codepen.io/anon/pen/RqKyQZ?editors=1010
You need to use mapStateToProps similar to the code below. Let say your reducer is called test and it is part of a state.
const mapStateToProps = (state, props) =>
({
router: props.router,
test: state.test
});
Then test will be used as a property in a React class. Obviously you need to include respective imports for React.
I have a very weird issue, I have redux store and a react component connected to it with connect() function. I am storing a list of user roles coming from my backend in redux. For this I have two different reducers, userRoleReducer and initialUserRolesReducer. I use the first one to handle changes into the roles in the UI before applying the changes with an API call, and the second one is being used to have the initial roles stored separately after backend responses. The issue I am having, is that both of the reducers are changing, even though only the first one is actually being updated by dispatching an action (Sorry if my use of terms is incorrect). Below are the reducers and action dispatchers.
Reducers:
export function userRolesForUsersRequestSuccess(state = {userRoles: []}, action) {
switch(action.type) {
case 'USER_ROLES_FOR_USERS_REQUEST_SUCCESS':
return action.userRoleDataForUsers;
default:
return state;
}
}
export function initialUserRolesForUsersRequestSuccess(state = {userRoles: []}, action) {
switch (action.type) {
case 'INITIAL_USER_ROLES_FOR_USERS_REQUEST_SUCCESS':
return action.initialUserRoleData;
default:
return state;
}
}
These are the action dispatchers, the first one is called from the connected component, and and after backend response. The second one is called only after the backend response.
export function setUserRolesForUsersRequestSuccess(userRoleDataForUsers) {
return {
type: 'USER_ROLES_FOR_USERS_REQUEST_SUCCESS',
userRoleDataForUsers
};
}
export function setInitialUserRolesForUsersRequestSuccess(initialUserRoleData) {
return {
type: 'INITIAL_USER_ROLES_FOR_USERS_REQUEST_SUCCESS',
initialUserRoleData
};
}
I haven't found anything similar to this from anywhere, so I guess this isn't a common problem, and that's why a good guess is that the issue is in my code. But every other reducer I use are working just fine, and believe me, I have tried to change and check everything I can to make these two work normally as well.
Any help is wanted to track the issue down!
EDIT: The code I use to create the store, not sure if it helps.
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './rootReducer';
import createHistory from 'history/createBrowserHistory';
const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const history = createHistory();
const middleware = routerMiddleware(history);
const initialState = {};
const store = createStore(
rootReducer,
initialState,
composeEnhancers(
applyMiddleware(middleware, thunk))
);
EDIT 2. rootReducer.js file, reducers are combined here.
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import {
userRoleForMeRequestSuccess,
userRolesForUsersRequestSuccess,
userRoleRequestPending,
userPermissionChangeGetResponseMessage,
initialUserRolesForUsersRequestSuccess } from './Common/userRoleReducer';
const appReducer = combineReducers({
userRoleForMeRequestSuccess,
userRolesForUsersRequestSuccess,
userRoleRequestPending,
userPermissionChangeGetResponseMessage,
initialUserRolesForUsersRequestSuccess,
router: routerReducer
});
const rootReducer = (state, action) => {
if (action.type === LOGIN_LOGOUT_REQUEST_SUCCESS) {
state = undefined;
}
return appReducer(state, action);
};
export default rootReducer;
EDIT 3. After I dug more deeply into this problem, I made an observation that if I just pass completely different data for the first reducer, its data stays intact and doesn't change when the other one changes. So could there be some kind of issue in passing exactly the same data as the first new state after the reducers initial state, and that mixes the reducers somehow to always mirror each other?