I have a normalized Redux store in my React Native application.
The structure of my reducer is:
{
byId: {},
allIds: []
}
In my component, I get the slice of Redux state using the useSelector hook:
const categories = useSelector((state: AppState) =>
state.products.allIds.map((id) => state.categories.byId[id.toString()])
);
The logic in the useSelector just converts the byId object into an array.
The infinite looping occurs when I set the categories array as a dependency:
const [values, setValues] = useState<any[]>([]);
useEffect(() => {
setValues([{ id: 1 }]);
console.log("logging");
}, [categories]);
Not sure what is the problem. I believe it's the useSelector logic that converts the objects into an array.
EDIT:
Full component code:
// React
import React, { useEffect, useState } from "react";
// React redux
import { useSelector } from "react-redux";
import { AppState } from "#reducers/rootReducer";
// Logic
import ProductsScreenLogic from "./ProductsScreen.logic";
// Common components
import ScreenView from "#common/screen/Screen.view";
// Components
import NewProductModalView from "#components/products/new-product-modal/NewProductModal.view";
import ProductsTabsView from "#components/products/products-tabs/ProductsTabs.view";
import ProductListView from "#components/products/products-list/ProductList.view";
import CategoryListView from "#components/products/category-list/CategoryList.view";
const ProductsScreenView: React.FC = () => {
const { displayProductList, setDisplayProductList, products } =
ProductsScreenLogic();
// Makes the categories ById object into an array of categories
const categories = useSelector((state: AppState) => state.categories.allIds.map((id) => state.categories.byId[id.toString()])
);
const [values, setValues] = useState<any[]>([]);
useEffect(() => {
setValues([{ id: 1 }]);
console.log("logging");
}, [categories]);
return (
<>
<NewProductModalView />
<ScreenView></ScreenView>
</>
);
};
export default ProductsScreenView;
The problem is that your selector always returns a new reference (because of the call to map). You could instead use createSelector which will memoize it and only return a new reference when something inside either allIds or byId changes:
const selectAllCategories = createSelector(
(state: AppState) => state.categories.allIds,
(state: AppState) => state.categories.byId,
(categoriesIds, categoriesById) => categoriesIds.map((id) => categoriesById[id.toString()])
);
But ideally you should avoid such selectors that go through the whole byId object, because it kind of negates the benefits of having a normalized state. You should rather have a parent component that only selects state.categories.allIds, and then passes the ids as props to child components, and every child component will select its own state.categories.byId[id]. This way if a category changes, only the corresponding child component will rerender, instead of having the parent and all of the children rerender.
In useEffect you update the state , wich cause render , render call useSelector wich every time return new array for useEffect , wich cause update the state. To fix you can remove categories from useEffect dependency array
Related
I'm trying to keep a live state of either the user is signed-in or not, so I can either show him or not, specific elements in the components. Using this code it's supposed to console.log(0) and then console.log(1), but it actually throws an error Cannot read properties of undefined.
./addons/Signed.js:
import { useState, createContext } from "react";
export const SignedContext = createContext();
export default function SignedProvider(props) {
const [SignedIn, setSignedIn] = useState(0);
return (
<SignedContext.Provider value={{ SignedIn, setSignedIn }}>
{props.children}
</SignedContext.Provider>
);
}
./screens/Profile.js:
import { useContext } from "react";
import SignedContext from "../addons/Signed";
...
const ProfileScreen = () => {
const { SignedIn, setSignedIn } = useContext(SignedContext);
console.log(SignedIn);
setSignedIn(1);
console.log(SignedIn);
...
}
...
You are using a named export for SignedContext but using a default import in Profile. Thus, you must use curly braces for your import. The following should change your issue.
import { SignedContext } from ".../addons/Signed"
Edit: If ProfileScreen is not a child of SignedContext.Provider, then this will not work. The general workflow is documented here. Hence, if ProfileScreen is not a child of the Provider, the context won't be available to it.
The ususal way to do this, is to define the context provider as a top level element in your app, if you want the context to be available at a global level in your application.
function App = () => {
const [signedIn, setSignedIn] = useState(0)
const contextValue = React.useMemo(() => ({signedIn, setSignedIn}), [singedIn])
// your application structure must be wrapped inside here.
// as an example I have only used ProfileScreen.
// Usually this is your root stack.
return (
<SignedContext.Provider value ={contextValue}>
<ProfileScreen />
</SignedContext.Provider>
)
}
store.js
import {useLocalObservable} from "mobx-react-lite";
function chatStore() {
return {
chatmsg: [],
setChatMsg(arr) {
this.chatmsg = arr
},
addChatMsg(msg) {
this.chatmsg.push(msg)
}
}
}
export const useChatStore = () => useLocalObservable(chatStore)
app.js
const App = () => {
const chatMsgStore = useChatStore()
const AppFunctions = {chatMsgStore}
useEffect(() => {
socket.on(activechat.chatid, (bdmsg) => {
chatMsgStore.addChatMsg(bdmsg)
})
return () => {
socket.off(activechat.chatid)
}
}, [activechat, chatMsgStore.chatmsg])
return (
<>
<AppContext.Provider value={AppFunctions}>
.....................
</AppContext.Provider>
</>
)
}
export default App;
fetch.js
async function getChatMessages(url, body, userStore, chatMsgStore) {
........
chatMsgStore.setChatMsg(firstResData)
........
on app load i add a socket listener which deps are activechat and chatMsgStore.
this listener is dynamic and must be changed when deps change.
the only purpose of this listener is to add a msg to the store and re-render the observer component
deps :
activechat - non store state
chatMsgStore.chatmsg - store state
why chatMsgStore.addChatMsg(bdmsg) does not effect the store? so deeply nested components inside App.js is not re-rendering.
otherwise i have a function getChatMessages which i import from custom hook deep inside App.js which sets the messages. this func is not a child of App.js and it is not wrapped with observer chatMsgStore.setChatMsg(firstResData) works! i can set the message so the observer component will re-render
how to make this code in useeffect above work?
Your App component is not wrapped with observer HOC so it won't react to observable values changes.
Wrap it like that:
const App = observer(() => {
// ...
})
or when exporting:
export default observer(App)
More info in the docs
you should use autorun from mobx in order to set correctly the reactivity in useEffect, here is a link to the doc that explains why and how use it.
But I think that you should not put chatMsgStore.chatmsg inside the deps array because you're not using it inside the useEffect.
If you can provide a working example maybe we can help you further.
I'm new in react native world and i'm on a new project with store (manage by redux).
I encounter an issue with custom hooks and useEffect
here my custom hooks
const useTheme = () => {
const [activeTheme, setActiveTheme] = useState();
const { id: universID, defaultTheme: universDefaultTheme } = useSelector(
(state) => state.univers
);
const { theme } = useSelector((state) => state);
const { themes: activeThemes } = useSelector((state) => state.settings);
const dispatch = useDispatch();
//set theme when univers change
useEffect(() => {
console.log('TODO TOO MANY CALLS!!!!!', universID);
if (universID) {
setTheme(
activeThemes.find((theme) => theme.univers === universID)?.theme
);
}
}, [universID]);
//get active theme of current univers
useEffect(() => {
setActiveTheme(
activeThemes.find((theme) => theme.univers === universID)?.theme
);
}, [activeThemes]);
... rest of code ...
return {
theme,
activeTheme,
setTheme,
};
}
on components i use
const {
theme: { colors },
} = useTheme();
My issue is that on every import the useEffect(()=>{},[universID]) is trigger. UniversID come from redux store.
If i understand clearly when i import useTheme() the reference of universID change because there are copy of universID from store created, and reference change.
if i pass universID as arguments to useTheme hooks there are no problem, cause reference is the same. But if i do this i need tu make a useSelector(universID) on every components who import useTheme hooks.
My understanding of mecanism is good ?
There are a way to get universID from store with the same reference on every import, for not trigger useEffect(,[universID]) on every import ? without pass universID as arguments of useTheme (i.e. useRef, useCallback) ?
Thanks for the time past to read (or better, to answer ;))
I am new to React Native Programming. So, please tell me in detail. thank you.
calling use Selector
I am calling use Selector inside my functional component like this:
import { useDispatch, useSelector } from 'react-redux';
const AddAddressScreen = ({ navigation }) => {
const dispatch = useDispatch();
const data = useSelector(state => state);
console.log(data + "happy Coding");
return (
<View style={styles.container}>
<View>
);
}
export default AddAddressScreen;
My reducer looks like this
case types.API_LOGIN_SUCCESS:
if (action.result.result.mobile_verified === false) {
return {
...state,
onLoad: false,
result: action.result,
status: action.status,
error: null,
navigation: action.navigation.navigate("VerifyMNO")
};
} else {
return {
...state,
onLoad: false,
result: action.result,
status: action.status,
error: null,
navigation: action.navigation.navigate("AddAddress")
};
}
here my mobile number is verified so I move to the address screen.
where I use Use Selector which gives me an error. while I remove above two lines my code runs successfully.
My saga looks like this
export function* watchLoginUserInfo() {
yield takeLatest(types.LOGIN_USER, loginApiSaga)
}
My root saga
import { all, fork } from 'redux-saga/effects';
import { watchLoginUserInfo, } from './authenticationSagas';
function* rootSaga() {
yield all([
watchLoginUserInfo(),
])
}
export default rootSaga;
My Store looks like this
import {createStore, applyMiddleware} from 'redux';
import rootReducer from '../redux/reducers/root-reducer.js'
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../redux/sagas/rootSaga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
export {store};
when ever I use use Selector hook in my code it gives me the following error.
error 1
error 2, 3, 4
Use the select effect from redux-saga inside of a reducer: https://redux-saga.js.org/docs/api/#selectselector-args
For example const selectedState = yield select(state => state);.
The useSelector hook is for use inside of a function component.
EDIT: since the above doesn't seem to be the issue, I think the issue is that you're calling navigation functions from within your reducer. Reducer code can have no side effects, so you can't call navigation.navigate(...) from within the reducer. This will need to happen in the saga code instead. It might be able to be done in the loginApiSaga or in a dedicated saga that is triggered by API_LOGIN_SUCCESS.
I have a custom reducer and a connected component to change its state. Now I'd like to use this state as a permanent filter on List elements.
I understand the List elements are connected to the redux-state, so I hope I'm able to access it through the List component's props, but couldn't find a way how to do that.
The List component is connected but not yours.
import { connect } from "react-redux";
const MyList = ({ is_published, ...props }) => (
<List {...props} filter={{ is_published }}>
</List>
);
const mapStateToProps = state => ({
is_published: state.myCustomReducer.is_published,
});
export default connect(mapStateToProps, undefined)(MyList);
Edit:
Just found out we don't update data when this prop change. This is a bug and you can open an issue about it.
In the mean time, here's a workaround:
Create a custom saga listening to whatever action you use alongside your custom reducer (I'll call it SET_IS_PUBLISHED for my example). This custom saga should put the changeListParams action creator from react-admin with your filter.
It will probably looks like this (not tested):
import { takeEvery, put, select } from 'redux-saga/effects'
import { changeListParams } from 'react-admin'
import { SET_IS_PUBLISHED } from './isPublished'
const getCurrentListParams = (state, resource) => {
const resourceState = state.admin.resources[resource]
return resourceState.list.params
}
function handleSetPublished({ payload }) {
const currentParams = yield select(getCurrentListParams)
const newParams = {
// Keep the current params
...currentParams,
// Override the filter
filter: {
// Keep the current filter
...currentParams.filter,
// Only override the is_published
is_published: payload
}
}
// Dispatch the action for the `posts` resource
yield put(changeListParams('posts', newParams))
}
export default function* () {
yield takeEvery(SET_IS_PUBLISHED, handleSetPublished)
}
just to bring this into 2021, you can use the useSelector redux hook to get hold of your custom state:
import { useSelector } from 'react-redux';
const MyCustomThing = (props) => {
const is_published = useSelector(state => state.customState.is_published);
}
For completeness, react-admin provides a customReducers prop to its <Admin> component so you can extend the redux state with your custom values:
const customStateReducer = (customState = { is_published: false }, { type, payload }) => {
if (type === 'IS_PUBLISHED') customState.is_published = payload.is_published;
return customState;
}
<Admin customReducers={{ customState: customStateReducer }} ...>
etc