I am using MobX-state-tree for state management and mst-persist to persist my data. The problem is when I reload the app the initial data of the store renders first and then the persist data gets loaded.
So whenever I want to check a persisted data in my store the initial data renders first, My function runs based on that and when everything stopped rendering my persisted data renders.
This code is a simplified version of my problem. When app renders first I get "false" in my console then I get "True". Even after I comment out the setTemp().
Is there any way to fix this or is there another package that I can use for persisting MST?
Rootstore.ts
import {
types,
Instance,
applySnapshot,
getSnapshot,
} from 'mobx-state-tree';
import {createContext, useContext} from 'react';
import AsyncStorage from '#react-native-async-storage/async-storage';
import {persist} from 'mst-persist';
const RootStore = types
.model('RootStore', {
temp: false,
})
.actions(store => ({
setTemp() {
applySnapshot(store, {...getSnapshot(store), temp: true});
},
}));
let _store: any = null;
export function initializeStore() {
_store = RootStore.create({});
persist('#initstore', _store, {
storage: AsyncStorage,
jsonify: true,
whitelist: ['temp'],
});
return _store;
}
export type RootInstance = Instance<typeof RootStore>;
const RootStoreContext = createContext<null | RootInstance>(null);
export const Provider = RootStoreContext.Provider;
export function useStore(): Instance<typeof RootStore> {
const store = useContext(RootStoreContext);
if (store === null) {
throw new Error('Store cannot be null, please add a context provider');
}
return store;
}
App.ts
import {initializeStore,Provider} from './src/store/RootStore';
const store = initializeStore();
<Provider value={store}>
<RootStack /> //the App
</Provider>
InitializeScreen.ts
import {observer} from 'mobx-react-lite';
import {useStore} from '../../store/RootStore';
const InitializeScreen = observer((): JSX.Element => {
const {setTemp,temp} = useStore();
useEffect(() => {
setTemp()
}, []);
console.log('init page',temp); // <-- Every time app reloads reneders false then true
return (
<Text>InitializeScreen</Text>
);
});
export default InitializeScreen;
Related
I am new to React Native and Redux, and was hoping someone could help out in my issue? I have a parent component that fetches some user data (their location) and dispatches to a redux store:
Parent
import { useDispatch } from 'react-redux'
import { setLocation } from './store/locationSlice'
const App = () => {
const dispatch = useDispatch()
const getLocation = () => {
const location = await fetchLoc()
dispatch(setLocation(location))
}
useEffect(() => {
getLocation()
},[])
}
My child component is intended to retrieve this data using the useSelector hook
Child
import { useSelector } from 'react-redux'
const HomeScreen = () => {
const location = useSelector(state => state.location)
useEffect(() => {
if (location) {
getEntitiesBasedOnLocation(location)
}
},[location])
}
However, in my case, useSelector never retrieves the up-to-date information that i have dispatched in the parent, with location returning undefined. I'm fairly certain there's a simple oversight here, but i'm at a loss as to what this could be. I was under the impression that useSelector subscribes to state changes, so why is it that that my dispatched action that causes a change of state is ignored? Using my debugger, I can see that my state is definitely updated with the correct data, but the child component doesn't pick this up..
Here's my location slice:
import { createSlice } from '#reduxjs/toolkit'
const initialState = {
location: {
id: null,
name: null,
latitude: null,
longitude: null
}
}
const locationSlice = createSlice({
name: 'location',
initialState,
reducers: {
setLocation: (state, action) => {
const { id, name, latitude, longitude } = action.payload
state.location = { id, name, latitude, longitude }
}
}
})
export const { setLocation } = locationSlice.actions
export default locationSlice.reducer
UPDATE
The store is configured by wrapping the App.js component in a Provider component, with the store passed as its props as follows:
Root.js
import { configureStore } from '#reduxjs/toolkit'
import { Provider } from 'react-redux'
import locationReducer from './src/store/locationSlice'
import App from './src/App'
const Root = () => {
const store = configureStore({ reducer: locationReducer })
return (
<Provider store={store)>
<App />
</Provider>
)
}
The issue is in your selector. You've created the slice called 'location' and within that slice you've got your state { location: {...}}. So from the perspective of the selector (which accesses your global state, not just the location slice) the path to your data would be state.location.location. But your selector is trying to read out of state.location which only has a location prop. Anything else you tried to read out would be undefined.
It is common to export a custom selection function from the slice configuration. Remember that the selector must take exactly the data that you want to share in your component tree (locationSlice.state.location in this case). This is not mandatory, it is just to facilitate development.
// locationSlice
import { createSlice } from '#reduxjs/toolkit'
//...
export const { setLocation } = locationSlice.actions
export const selectLocation = (state) => state.location.location
export default locationSlice.reducer
// Child
import { useSelector } from 'react-redux'
import {selectLocation} from './src/store/locationSlice'
const HomeScreen = () => {
const location = useSelector(selectLocation)
//...
}
My workaround was to move my getLocation() function in the parent to the child component. useSelector now gets the state as expected. I feel that this work-around defeats the object of having global state access though, and i could probably just use local state rather than Redux.
In my React project I'm using redux toolkit and all is working fine on the UI side, but I'm running into the following error now I've come to testing my code (Note - I have chosen RTL as my library of choice, and I know I should test as I write code but I'm here now anyway).
TypeError: Cannot read property 'isSearching' of undefined
16 | const postState = useSelector(selectInitialPostsState);
> 17 | const isSearching = postState.isSearching;
As can be seen above, I import my initialState and save it to postState in my component, and then access isSearching from there - which works absolutely fine in my code and my App. I have accessed my state properties this way throughout my code as it seemed easier than having to write an individual selector for each state property used.
In my test file, I have resorted to Redux's test writing docs and have tried two different ways of rendering my store via manipulating RTL's render() method. The first function written locally in my test file, which for the time being I have commented out, and the second is imported into my test file from test-utils. Both methods give me the same errors.
Here is my test file:
import React from 'react';
// import { Provider } from 'react-redux';
// import { render as rtlRender, screen, fireEvent, cleanup } from '#testing-library/react';
// import { store } from '../../app/store';
import { render, fireEvent, screen } from '../../../utilities/test-utils';
import Header from '../Header';
// const render = component => rtlRender(
// <Provider store={store}>
// {component}
// </Provider>
// );
describe('Header Component', () => {
// let component;
// beforeEach(() => {
// component = render(<Header />)
// });
it('renders without crashing', () => {
render(<Header />);
// expect(screen.getByText('Reddit')).toBeInTheDocument();
});
});
And here is my test-utils file:
import React from 'react';
import { render as rtlRender } from '#testing-library/react';
import { configureStore } from '#reduxjs/toolkit';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
// Import your own reducer
import sideNavReducer from '../src/features/sideNavSlice';
import postReducer from '../src/features/postSlice';
function render(
ui,
{
preloadedState,
store = configureStore({ reducer: { sideNav: sideNavReducer, posts: postReducer }, preloadedState }),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return (
<Provider store={store}>
<BrowserRouter>{children}</BrowserRouter>
</Provider>
)
}
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}
// re-export everything
export * from '#testing-library/react'
//override render method
export { render }
I have been able to prevent this error by creating a selector that saves the property directly, and accessing it in my component as isSearching without having to use postState.isSearching, but then the next error comes up about the next property I have to do this with, and then the next and so on.
It seems that in my test file, postState is undefined, whereas in my console, it holds the initialState from my slice file. Can anyone please advise why this is? I don't see a reason why I would have to write numerous selectors that directly access properties, rather than accessing them through an initialState selector. I can't seem to grasp how it works in my functioning code but not in my test code.
For further context if required, here is the mentioned initialState and selector from my slice file:
const postSlice = createSlice({
name: 'posts',
initialState: {
allPosts: [],
popularPosts: [],
sportPosts: [],
newsPosts: [],
savedPosts: [],
hiddenPosts: [],
reportedPosts: [],
dataLoading: true,
allPostsShown: true,
ellipsisClicked: false,
reportModal: false,
modalClosed: true,
imgClicked: false,
searchedPostsFound: false,
searchedPosts: [],
isSearching: false,
searchText: "",
},
});
export const selectInitialPostsState = state => state.posts;
And here is how it is used in my component. I have only included the relevant code.
const SideNav = () => {
const postState = useSelector(selectInitialPostsState);
const isSearching = postState.isSearching;
useEffect(() => {
!isSearching && setSearchText("");
if(!searchText){
dispatch(userNoSearch());
dispatch(searchedPostsFound({ ids: undefined, text: searchText })); // reset searchedPosts state array
}
}, [dispatch, searchText, isSearching]);
useEffect(() => {
if(isSearching){
dispatch(searchedPostsFound());
}
}, [dispatch, isSearching, search]);
}
If you want to load the initialState of your component, you need to send it as a parameter to rtlRender with your component.
it('renders without crashing', () => {
rtlRender(<Header />, { preloadedState: mockStateHere });
// expect(screen.getByText('Reddit')).toBeInTheDocument();
});
your mockStateHere structure needs to resemble what your redux store's state looks like as best as possible, using entities and id's etc
If you look at how you are building the render function in this example, you'll see the param preloadedState being deconstructed along with store which has the default configuration in this example:
function render(
ui,
{
preloadedState,
store = configureStore({ reducer: { sideNav: sideNavReducer, posts: postReducer }, preloadedState }),
...renderOptions
} = {}
...
)
that's the same preloadedState value that you send in with your ui component, which is being loaded into your store with the configureStore method and that store value will be sent into the Provider
We are currently using next.js to ssr. Refreshing or going back on a specific page initializes all stores.
I've been thinking about mobx-persist to solve this problem, but mobx-persist seems to only work in a browser environment.
Here's the question.
Is there a way to keep the value of a particular element in the store whenever I refresh the browser without using mobx-persist?
Is there a way to get the retained elements of the store from the ssr environment?
Cookies, sessions, mobx-persist were not available in the ssr environment.
Below is the current react setting.
app.js
import React from 'react'
import { Provider } from 'mobx-react'
import { UserStore } from '../stores/userStore'
//_app.js
const App = ({ Component, pageProps }) => {
const userStore = UserStore(pageProps.initialState)
return (
<Provider
UserStore={userStore}
>
<Component {...pageProps} />
</Provider>
)
}
export default App
userStore.js
const isServer = typeof window === 'undefined'
useStaticRendering(isServer)
class Store {
#observable initialData = null
#observable userId = null
#observable userPw = null
// #action...
hydrate = async (initialState) => { // SSR INIT
// this.initialState = initialState
}
export function initUserStore(initialState = null) {
this.hydrate(initialState)
// return store
})
export function UserStore(initialState) {
const store = useMemo(() => initUserStore(initialState), [initialState])
return store
}
}
index.js (ssr)
// index.js ssr
export async function getServerSideProps(context) {
const UserStore = initUserStore() // store init
// How can I get the value of the element (userId, userPw) of the store when it is in a refresh or server-side environment?
.
.
.
return {
// props :{
// initialState:{
// }
}
}
})
The key issue is to import elements stored in the in the getServerSideProps(ssr) environment without store initialization after refresh.
I'm currently trying to create an application that will serve a client side web application that will be mostly the same, but slightly different based on the different subdomains. For example, client1.website.com vs client2.website.com -- same base app, slightly different branding and content.
Currently, I have attempted to save this content by fetching static JSON files that contain the differences between the two sites. They are made as a normal GET call to the local server and then applied as a 'content' key in the state object of the application. The problem I am having is that I am unable to reference parts of the content because React attempts to render them before the parent application is finished applying the 'content' state.
Is this architecture the wrong way to go about things or is there a solution that I just haven't found yet?
I've posted my code below to try and show what I'm doing, I've used a third party state library to try and simplify what something like redux would do when full fleshed out:
// Index.js (Contains my store)
import React from "react";
import axios from "axios";
import ReactDOM from "react-dom";
import { StoreProvider, createStore, thunk, action } from "easy-peasy";
import "./index.css";
import App from "./pages/App";
const store = createStore({
company: {
contentType: "default",
baseContent: {},
partnerContent: {},
setContentType: action((state, payload) => {
state.contentType = payload;
}),
setContentData: action((state, payload) => {
state[payload.type] = payload.data;
}),
loadContent: thunk(async (actions, payload) => {
try {
if (payload !== "base") {
actions.setContentType(payload);
}
const partnerData = await axios.get(`content/${payload}.json`);
const baseData = await axios.get(`content/base.json`);
actions.setContentData({
data: partnerData.data,
type: "partnerContent",
});
actions.setContentData({
data: baseData.data,
type: "baseContent",
});
} catch (e) {
throw e;
}
}),
},
});
ReactDOM.render(
<StoreProvider store={store}>
<App />
</StoreProvider>,
document.getElementById("root")
);
//App.js (Where I attempt to suspend until my data is loaded into state)
import React, { useEffect, Suspense } from "react";
import { useStoreActions } from "easy-peasy";
import Header from "../components/Header";
import Content from "../components/Content";
import "./App.css";
const content = "default";
const App = () => {
const { loadContent } = useStoreActions((actions) => ({
loadContent: actions.payforward.loadContent,
}));
useEffect(() => {
async function fetchData() {
await loadContent(content);
}
fetchData();
}, [loadContent]);
return (
<div className='App'>
<Header />
<Content />
</div>
);
};
export default App;
//Header.js (Where I attempt to reference some URLs from the JSON file applied to my state)
import React, { useMemo } from "react";
import { useStoreState } from "easy-peasy";
const Header = () => {
//Unable to access state because it's currently undefined until the JSON is loaded in
const headerURLs = useStoreState(
(state) => state.company.partnerContent.routes.header.links
);
return (
<div>
<h1>This is the Header</h1>
{/* {headerURLs.map((url) => {
return <p>{url}</p>;
})} */}
</div>
);
};
export { Header as default };
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)