I have a React app that has multiple tabs. When the user goes to the "Data" tab it will fetch data from an API call and set the data in a React state. However, if the user goes from "Data" tab to "Home" tab then back to "Data" tab it will have to fetch the data again from API call because data in state has disappeared.
Psuedocode of desired functionality:
const OutputTab: React.FC<PageProps> = ({ match, pageName }) => {
const [outputData, setOutputData] = useState<outputsInterface[]>([]);
useIonViewWillEnter(() => {
if (!outputData) {
fetchOutputs();
}
});
const fetchOutputs = () => {
let response = fetch("....");
setOutputData(response.json);
};
};
What is the simplest way to store the state data? Desired functionality is when user comes back to the tab we can simply check if data already exists rather than making another API call to refetch.
I thought of possible solutions to use localStorage or sessionStorage but I'd prefer to store the data in memory rather than storage. Do I need something like Redux to accomplish this?
full solution with video examples here with source code in codesandbox
https://youtu.be/DiCzp5kIcP4
https://dev.to/aaronksaunders/two-ways-to-manage-state-in-react-js-5dkb
Using Context API
import React from "react";
// create the context
export const Context = React.createContext();
// create the context provider, we are using use state to ensure that
// we get reactive values from the context...
export const TheProvider = ({ children }) => {
// the reactive values
const [sharedValue, setSharedValue] = React.useState({
value: "initial",
changedBy: "Admin"
});
// the store object
let state = {
sharedValue,
setSharedValue
};
// wrap the application in the provider with the initialized context
return <Context.Provider value={state}>{children}</Context.Provider>;
};
export default Context;
Using Reducer with useReducer
const reducer = (state: IState, action: ActionType): IState => {
switch (action.type) {
case "update":
return { ...state, ...action.payload };
case "clear":
return { ...state, ...action.payload, value: "" };
default:
throw new Error();
}
};
const [state, dispatch] = React.useReducer(reducer, initialState);
Related
I have a redux state that contains an array of objects, for each of these object I call an api to get more data
objects.forEach((obj, index) => {
let newObj = { ...obj };
service.getMoreData()
.then(result => {
newObj.data = result;
let newObjects = [...this.props.objectsList] ;
let index = newObjects.findIndex(el => el.id === newObj.id);
if (index != -1) {
newObjects[index] = newObj;
this.props.updateMyState({ objectsList: newObjects });
}
})
When I get two very close responses the state is not updated correctly, I lose the data of the first response.
What is the right way to update a single element of the array? Thanks!
So since i don't know what service is and there isn't that much here to go off, here is what I would do from my understanding of what it looks like your doing:
So first let's set up a reducer to handle the part of redux state that you want to modify:
// going to give the reducer a default state
// array just because I don't know
// the full use case
// you have an id in your example so this is the best I can do :(
const defaultState = [{ id: 123456 }, { id: 123457 }];
const someReducer = (state=defaultState, action) => {
switch (action.type) {
// this is the main thing we're gonna use
case 'UPDATE_REDUX_ARRAY':
return [
...action.data
]
// return a default state == the state argument
default:
return [
...state
]
}
}
export default someReducer;
Next you should set up some actions for the reducer, this is optional and you can do it all inline in your component but I'd personally do it this way:
// pass data to the reducer using an action
const updateReduxArray = data => {
return {
type: 'UPDATE_REDUX_ARRAY',
data: data
}
}
// export like this because there might
// be more actions to add later
export {
updateReduxArray
}
Then use the reducer and action with React to update / render or whatever else you want
import { useState } from 'react';
import { updateReduxArray } from 'path_to_actions_file';
import { useEffect } from 'react';
import { axios } from 'axios';
import { useSelector, useDispatch } from 'react-redux';
const SomeComponent = () => {
// set up redux dispatch
const dispatch = useDispatch();
// get your redux state
const reduxArray = useSelector(state => state.reduxArray) // your gonna have to name this however your's is named
// somewhere to store your objects (state)
const [arrayOfObjects, updateArrayOfObjects] = useState([]);
// function to get data from your API
const getData = async () => {
// I'm using axios for HTTP requests as its pretty
// easy to use
// if you use map you can just return the value of all API calls at once
const updatedData = await Promise.all(reduxArray.map(async (object, index) => {
// make the api call
const response = axios.get(`https://some_api_endpoint/${object.id}`)
.then(r => r.data)
// return the original object with the addition of the new data
return {
...response,
...object
}
}))
// once all API calls are done update the state
// you could just update redux here but this is
// a clean way of doing it incase you wanna update
// the redux state more than once
// costs more memory to do this though
updateArrayOfObjects(updatedData)
}
// basicity the same as component did mount
// if you're using classes
useEffect(() => {
// get some data from the api
getData()
}, [ ])
// every time arrayOfObjects is updated
// also update redux
useEffect(() => {
// dispatch your action to the reducer
dispatch(updateReduxArray(arrayOfObjects))
}, [arrayOfObjects])
// render something to the page??
return (
<div>
{ reduxArray.length > 0
? reduxArray.map(object => <p>I am { object.id }</p>)
: <p>nothing to see here</p>
}
</div>
)
}
export default SomeComponent;
You could also do this so that you only update one object in redux at a time but even then you'd still be better off just passing the whole array to redux so I'd do the math on the component side rather than the reducer .
Note that in the component I used react state and useEffect. You might not need to do this, you could just handle it all in one place when the component mounts but we're using React so I just showcased it incase you want to use it somewhere else :)
Also lastly I'm using react-redux here so if you don't have that set up (you should do) please go away and do that first, adding your Provider to the root component. There are plenty of guides on this.
I have a todo app that does all 4 crud operations but I can't filter them based on their current status here's the app on codesandbox.
import { SET_VISIBILITY_FILTER } from "../actionTypes";
const initialState = {
filters: ["SHOW_ALL"]
};
const visibilityFilter = (state = initialState, { type, payload }) => {
switch (type) {
case SET_VISIBILITY_FILTER:
return { payload };
default:
return state;
}
};
export default visibilityFilter;
Any explanations will be appreciated.
I have also checked other react redux todo app github repos but most of them are old and it didn't look like they were writing in the best possible way, so I am trying to find a better way (and so far failing at it)
A few issues
filters is an array in the initial state, but you send single values there after in your action, and you also use it a single value when filtering with it.
you expect payload in your reducer but the data you dispatch does not wrap things in payload
dispatch({
type: SET_VISIBILITY_FILTER,
filter
});
in continuation to the above you should use the already defined action setFilter for setting a filter, which correctly wrap the data in a payload property.
fixing these 3 issues, you get https://codesandbox.io/s/problems-with-redux-forked-hv36h which is working as intended.
What you are doing is an anti-pattern when you mutate the redux state variable inside the component like this:
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case "SHOW_ALL":
return todos;
case "SHOW_COMPLETED":
return todos.filter((t) => t.completed);
case "SHOW_ACTIVE":
return todos.filter((t) => !t.completed);
default:
return todos;
}
};
Instead what you should do, listen to the SET_VISIBILITY_FILTER action on toDoReducer.js:
//import SET_VISIBILITY_FILTER action
case SET_VISIBILITY_FILTER:
let toDoClone = [...state.todos]
//if(filter = something)
toDoClone.filter(t => //your condition)
return {
...state,
todos: toDoClone
}
I'm a newbie with Next JS.
I use Next JS and Redux.
I have a short code below:
const AdminContainer = (props) => {
return (
<AdminMasterView>
<DashboardView studentList={props.studentListServer}/>
</AdminMasterView>
)
}
export const getStaticProps = (async () => {
let response = await db.getInstance().query('SELECT * FROM student_register;');
return {
props: {
studentListServer: response
}, // will be passed to the page component as props
}
})
const mapStateToProps = state => ({
studentList: state.studentInfoReducers.studentList
});
const mapDispatchToProps = {
getStudentRegisterAction
};
export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer);
I also have studentList (array type) props is declare in Redux. I want to use it to pass data because I have many tasks to do with data such as filter, order,...
Is there any way to use studentList like this and my app still is server rendering first time.
If I dispatch studentListServer to studentList, it still work. But my app isn't server rendering.
<DashboardView studentList={props.studentList}/>
Or easier, I'll check to use props.studentList for client-side and props.studentListServer for server-side. But I think it's not good.
Thank you so much!
You could use the next-redux-wrapper package. It allows to sync a Redux state on server and client. Consider the example:
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
let response = await db.getInstance().query('SELECT * FROM student_register;');
// dispatch the action that saves the data
store.dispatch({ type: 'SET_STUDENTS', payload: response });
return {
props: {
studentListServer: response
}, // will be passed to the page component as props
}
})
wrapper.getStaticProps wraps your getStaticProps function with the new parameter store that is a Redux store in fact.
Action with type SET_STUDENTS sets the student list on a server side. When Next.js generates the page, it will save this data in static JSON. So when the page opens on client side, next-redux-wrapper recreates a state dispatching HYDRATE action with saved on a build time static JSON that you can use to restore the studentInfoReducers reducer.
E.g. in your reducer you should implement something like:
import { HYDRATE } from 'next-redux-wrapper';
const initialState = { studentList: [] };
// studentInfoReducers reducer
function reducer(state = initialState, action) {
// this sets your student list
if (action.type === 'SET_STUDENTS') {
return {
...state,
studentList: action.payload,
};
}
// this rehydrates your store from server on a client
if (action.type === HYDRATE) {
return action.payload.studentInfoReducers;
}
return state;
}
So afterwards you should have a valid synced state on client and server at the same time:
const mapStateToProps = state => ({
studentList: state.studentInfoReducers.studentList // works on server and client
});
Let me know if you have any questions, next-redux-wrapper can be tricky from a first look.
You don't need to use Redux for that.
Using just cookies you can achieve bidirectional communication, see https://maxschmitt.me/posts/next-js-cookies/
Another example:
Client to Server: manually set a cookie in the client side and then read it in the server with req.headers.cookie or some library like 'cookie'
Server to Client: just read the cookie, and return what you need as a regular prop or update the cookie.
import { useState, useEffect } from "react";
import Cookie from "js-cookie";
import { parseCookies } from "../lib/parseCookies";
const Index = ({ initialRememberValue = true }) => {
const [rememberMe, setRememberMe] = useState(() =>
JSON.parse(initialRememberValue)
);
useEffect(() => {
//save/create the cookie with the value in the client
Cookie.set("rememberMe", JSON.stringify(rememberMe));
}, [rememberMe]);
return (
<div>
remember me
<input
type="checkbox"
value={rememberMe}
checked={rememberMe}
onChange={e => setRememberMe(e.target.checked)}
/>
</div>
);
};
Index.getInitialProps = ({ req }) => {
//read the cookie on the server
const cookies = parseCookies(req); //parseCookies is a simple custom function you can find
return {
//send the value as a regular prop
initialRememberValue: cookies.rememberMe
};
};
export default Index;
Reference: https://github.com/benawad/nextjs-persist-state-with-cookie/blob/master/pages/index.js
I have a form composed of several input components. The form data is shared and shareable across these sibling components via a React context and the React hook useContext.
I am stumbling on how to optionally async load data into the same context. For example, if the browser URL is example.com/form, then the form can load with the default values provided by a FormContext (no problem). But if the user is returning to finish a previously-edited form by navigating to example.com/form/:username/:form-id, then application should fetch the data using those two data points. Presumably this must happen within the FormContext somehow, in order to override the default empty form initial value.
Are url params even available to a React.createContext function?
If so, how to handle the optional data fetch requirement when hooks are not to be used with in a conditional?
How to ensure that the data fetch occurs only once?
Lastly, the form also saves current state to local storage in order to persist values on refresh. If the data fetch is implemented, should a refresh load the async data or the local storage? I'm leaning towards local storage because that is more likely to represent what the user last experienced. I'm open to opinions and thoughts on implementation.
FormContext
export const FormContext = React.createContext();
export const FormProvider = props => {
const defaultFormValues = {
firstName: "",
lastName: "",
whatever: "",
};
const [form, setForm] = useLocalStorage(
"form",
defaultFormValues
);
return (
<FormContext.Provider value={{ form, setForm }}>
{props.children}
</FormContext.Provider>
);
};
Reference for useLocalStorage
I think the answer you're looking for is Redux, not the library but the workflow. I did find it curious React doesn't give more guidance on this. I'm not sure what others are doing but this is what I came up with.
First I make sure the dispatch from useReducer is added to the context. This is the interface for that:
export interface IContextWithDispatch<T> {
context: T;
dispatch: Dispatch<IAction>;
}
Then given this context:
export interface IUserContext {
username: string;
email: string;
password: string;
isLoggedIn: boolean;
}
I can do this:
export const UserContext = createContext<IContextWithDispatch<IUserContext>>({
context: initialUserContext,
dispatch: () => {
return initialUserContext;
},
});
In my top level component I memoize the context because I only want one instance. This is how I put it all together
import memoize from 'lodash/memoize';
import {
IAction,
IContextWithDispatch,
initialUserContext,
IUserContext,
} from './domain';
const getCtx = memoize(
([context, dispatch]: [IUserContext, React.Dispatch<IAction>]) =>
({ context, dispatch } as IContextWithDispatch<IUserContext>),
);
const UserProvider = ({ children }) => {
const userContext = getCtx(useReducer(userReducer, initialUserContext)) as IContextWithDispatch<
IUserContext
>;
useEffect(() => {
// api call to fetch user info
}, []);
return <UserContext.Provider value={userContext}>{children}</UserContext.Provider>;
};
Your userReducer will be responding to all dispatch calls and can make API calls or call another service to do that etc... The reducer handles all changes to the context.
A simple reducer could look like this:
export default (user, action) => {
switch (action.type) {
case 'SAVE_USER':
return {
...user,
isLoggedIn: true,
data: action.payload,
}
case 'LOGOUT':
return {
...user,
isLoggedIn: false,
data: {},
}
default:
return user
}
}
In my components I can now do this:
const { context, dispatch } = useContext<IContextWithDispatch<IUserContext>>(UserContext);
where UserContext gets imported from the export defined above.
In your case, if your route example.com/form/:username/:form-id doesn't have the data it needs it can dispatch an action and listen to the context for the results of that action. Your reducer can make any necessary api calls and your component doesn't need to know anything about it.
Managed to accomplish most of what I wanted. Here is a gist demonstrating the basic ideas of the final product: https://gist.github.com/kwhitejr/df3082d2a56a00b7b75365110216b395
Happy to receive feedback!
I have a React app that uses React-Router/React-Router-dom for page navigation and redux to store some global state info (jwt token for django rest framework for example). The state also stores info about the currently viewed page, such as the serialized django model.
But what is the best way to load the django model into the redux store when the route changes? I'm having trouble wrapping my head around where logic should be going.
If you view the repo below you can see where I'm having trouble figuring it out.
In this example when someone navigates to /spells/:id, it should load the spell django model into the redux store so information about it is globally accessible.
But how do I go about doing that? Where do I call the actions and reducers to properly handle the state?
Any guidance would be appreciated.
You can view the full project here. The component in question here is LayoutSpellView (/frontend/src/components/LayoutSpellView). That's where the model information is stored, displayed, etc.
Edit: Adding relevant code
Called in componentDidMount:
axios
.get("http://localhost:3000/api/spells/" + spellId)
.then(response => {
let spell = Object.assign({}, spellView.state.spell);
spell.id = response.data.id;
spell.owner = response.data.owner;
...blahblah other fields
this.setState({
spell
});
})
.then(response => {
this.props.dispatch({
type: 'FETCH_SPELL_SUCCESS',
payload: this.state.spell,
});
})
.catch(function(error) {
console.error('[API]\t', error);
});
In LayoutSpellView (same component as above)
import {loadSpell} from "../src/reducers";
const mapStateToProps = (state) => ({
spell: loadSpell(state.spell.id),
});
const mapDispatchToProps = (dispatch) => ({
getSpell: (state.spell.id) => {
dispatch(loadSpell(state.spell.id))
}
});
Actions spell.js:
export const FETCH_SPELL = '##spell/FETCH_SPELL';
export const FETCH_SPELL_SUCCESS = '##spell/FETCH_SPELL_SUCCESS';
export const FETCH_SPELL_FAILURE = '##spell/FETCH_SPELL_FAILURE';
export const loadSpell = (spellId) => ({
[RSAA]: {
endpoint: '/api/spell/${spellId}',
method: 'GET',
types: [
FETCH_SPELL, FETCH_SPELL_SUCCESS, FETCH_SPELL_FAILURE
]
}
});
Reducers spell.js:
const initialState = {
spell: {
id: 0,
owner: 0,
Name: 'Name',
School: 'unknown',
Subschool: 'unknown',
}
};
export default (state=initialState, action) => {
switch(action.type) {
case spell_action.FETCH_SPELL_SUCCESS:
return {
spell: {
id: action.payload.spell.id,
owner: action.payload.spell.owner,
Name: action.payload.spell.Name,
School: action.payload.spell.School,
Subschool: action.payload.spell.Subschool,
}
};
default:
return state;
}
}
export function loadSpell(state) {
if (state) {
return state.spell
}
}
Let's look at the question in a different way. Instead of asking "How do I dispatch an action when routes change", let's ask "What is the actual source of truth: Redux or URL?"
If we go with redux being the Single Source of Truth, then that would mean that we need to dispatch some action that would cause some side-effect ( maybe redux-saga or redux-observable or even redux-thunk? ) that changed the url:
Comp -> dispatch(action) -> store updates -> URL changes
If we go with the URL being the Single Source of Truth, we change the flow to:
URL changes -> dispatch(action) -> store updates
If we go this route, which is what it sounds like you are wanting, you will need to probably hook up middleware, which are functions of the following signature:
store => next => action => next(action)
Depending on the router that you are using, you can either hook into their actions or you can hook into window.onpopstate and check the next url. Either way, the overall middleware function would look something like
const middleware = store => {
return next => action => {
if (actionWillCauseSpellToBeNeeded(action)) {
makeAPICall()
.then(transformAPIToAction)
.catch(transformError)
.then(store.dispatch)
}
return next(action)
}
}