I'm having a table of employees and their monthly working hours day by day. Here we can update all the employees hours values in bulk.
A simple math for the current month: 30 days x 50 employees will result in 1500 mounted Redux Form Fields.
Each time a Field is being mounted, Redux Form will dispatch an action for registering the Field in the Redux store. Therefore 1500 events are dispatched.
Debugging with Chrome->Performance tool, I found out that the whole process from mounting through dispatching to rendering the Fields takes about ~4 seconds:
The above performance score is based on the following simple working example I've created with React, Redux, Redux Form, Reselect:
/* ----------------- SAMPLE DATA GENERATION - START ----------------- */
const generateEmployees = length =>
Array.from({ length }, (v, k) => ({
id: k + 1,
name: `Emp ${k + 1}`,
hours: [8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8]
}))
const employees = generateEmployees(50)
/* ----------------- SAMPLE DATA GENERATION - END ------------------- */
/* --------------------- INITIALIZATION - START --------------------- */
const { reduxForm, Field, reducer: formReducer } = ReduxForm
const { createStore, combineReducers, applyMiddleware } = Redux
const { Provider, connect } = ReactRedux
const { createSelector } = Reselect
// Creating the Reducers
const employeesReducer = (state = employees) => state
const reducers = {
form: formReducer,
employees: employeesReducer
}
// Custom logger.
// The idea here is log all the dispatched action,
// in order to illustrate the problem with many registered fields better.
const logger = ({ getState }) => {
return next => action => {
console.log("Action: ", action)
return next(action)
}
}
const reducer = combineReducers(reducers)
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
applyMiddleware(logger)
)
/* --------------------- INITIALIZATION - END ----------------------- */
const renderEmployees = employees =>
employees.map(employee => {
return (
<tr key={employee.id}>
<td>{employee.id}</td>
<td>{employee.name}</td>
{employee.hours.map((hour, day) => (
<td key={day}>
<Field component="input" name={`${employee.id}_${day}`} />
</td>
))}
</tr>
)
})
const FormComponent = ({ employees, handleSubmit }) => {
return (
<form onSubmit={handleSubmit}>
<h2>Employees working hours for November (11.2018)</h2>
<p>
<button type="submit">Update all</button>
</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
{Array.from({ length: 30 }, (v, k) => (
<th key={k + 1}>{`${k + 1}.11`}</th>
))}
</tr>
</thead>
{renderEmployees(employees)}
</table>
</form>
)
}
const Form = reduxForm({
form: "workingHours",
onSubmit: submittedValues => {
console.log({ submittedValues })
}
})(FormComponent)
const getInitialValues = createSelector(
state => state.employees,
users =>
users.reduce((accumulator, employee) => {
employee.hours.forEach(
(hour, day) => (accumulator[`${employee.id}_${day}`] = hour)
)
return accumulator
}, {})
)
const mapStateToProps = state => ({
employees: state.employees,
initialValues: getInitialValues(state)
})
const App = connect(mapStateToProps)(Form)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
)
table {
border-collapse: collapse;
text-align: center;
}
table, th, td {
border: 1px solid black;
}
th {
height: 50px;
padding: 0 15px;
}
input {
width: 20px;
text-align: center;
}
<script src="https://unpkg.com/react#15.5.4/dist/react.js"></script>
<script src="https://unpkg.com/react-dom#15.5.4/dist/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.4/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-form/6.7.0/redux-form.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/3.0.1/reselect.js"></script>
<div id="root">
<!-- This element's contents will be replaced with your component. -->
</div>
So am I doing something inefficient and wrong with Redux Form or rendering a large number of Fields at once is bottleneck and I should take a different approach (pagination, lazy loading, etc.)?
Existing open-source form libraries seem to not work well when getting into this kind of complexity. We implemented a number of optimisations by building our own form library, but it's possible that one or more of these ideas you may be able to use with redux form.
We mount a multitude of form & other redux attached components (in the order of thousands), which all do validation when they get mounted. Our architecture required all components to be mounted at once. Each component may need to update the state multiple times when they get mounted. This is not quick, but these are the optimisations we used to make this seem quick for 1000+ controls:
Each component mounts in a way which checks if it really needs to dispatch something right now, or if it can wait until later. If the value in the state is wrong, or the current value is invalid - it will still cause a dispatch, otherwise it will defer other updates for later (like when the user focuses it).
Implemented a batch reducer which can process multiple actions in a single dispatch. This means if a component does need to perform several actions on mount, at most it will only send 1 dispatch message instead of one dispatch per action.
Debouncing react re-renders using a redux batch middleware. This means that react will be triggered to render less often when a big redux update is happening. We trigger listeners on the leading and the falling edge of redux updates, which means that react will update on the first dispatch, and every 500-1000ms after that until there are no more updates happening. This improved performance for us a lot, but depending on how your controls work, this can also make it look a bit laggy (see next item for solution)
Local control state. Each form control has a local state and responds to user interaction immediately, and will lazily update the redux state when the user tabs out of the control. This makes things seem fast and results in less redux updates. (redux updates are expensive!!)
I'm not sure any of this will help you, and I'm not even sure that the above was smart or recommended - but it worked really well for us and I hope it can give you some ideas.
Also, if you can get away with mounting less components (pagination etc), you definitely should do that instead. We were limited in our choices by some early architecture choices and were forced to press on and this is what we came up with.
Did you try https://www.npmjs.com/package/react-virtualized I used it in a project where I was capturing dozen of events. The list is growing and growing and this component helped me render all of them. I'm not sure how Redux Form works but if it is based on mounting then I guess this is a good option.
Related
Hello i'm trying to create some kind of lottery and i'm wondering which approach of modifying state by actions payload should be used.
Let's say i have state
type initialCartState = {
productsFromPreviousSession: Product[]
selectedProduct: Product
balance: number,
productsInCart: Product[]
}
and our reducer looks like
const reducers = {
addProduct(state, action) => {
state.products.push(state.action.payload.product)
},
addProductsFromPreviousSession(state, action) => {
state.products.push(...state.productsFromPreviousSession)
},
}
And i noticed i used completely two different approaches with these two types cuz in my component it looks like
const component = () => {
const selectedProduct = useSelector(state => state.cart.selectedProduct);
const availableBalance = useSelector(state => state.cart.balance - sum(state.cart.products, 'price'));
const dispatch = useDispatch()
const sumOfProductsFromPreviousSession = useSelector(state => sum(state.cart.products,'price'))
return (
<div>
<div onClick={() => {
if((balance - selectedProduct.price) > 0) {
dispatch(cartActions.addProduct(selectedProduct))
}
}}/>
<div onClick={() => {
if((balance - sumOfProductsFromPreviousSession) > 0) {
dispatch(cartActions. addProductsFromPreviousSession())
}
}}/>
</div>
)
}
There are two different types of handling actions, in addProduct i used selector and pass value in action payload. In Add products from previous session we rely on state inside reducer (Also have middleware for purpose of saving in localStorage, but there i used store.getState()). Which kind of approach is correct ?
Also how it will change when we move balance to another reducer, and then we will not have access to that i cartReducer?
I saw there are bunch of examples on counter when increment and decrement rely on current reducerState and there are actions without payload, but there is no validation which is used in my example.
Thanks in advance !
Both approaches can be used. Basically, if you need to show state data in UI or other parts of processes, you should read with selector. This way, changes inside the store can be reflected in the components reactively.
In your case, you are just updating the state value with currently available data from the state. So, you can dispatch action without payload.
In your example, even though you pass the payload from onClick event, you are still reading the value from the state itself.
Hello friendly people of stackoverflow, I'm having a map function issue and I just can't put my finger on what the problem could be. Context is just for user data and its not needed to replicate the issue, context/state can be removed or changed so I'm not so sure it's even a state issue. Strict mode changes do nothing. Id/key changes aren't helping. I've tried every iteration from good to bad of map key/id practices (using the best answers on the website for map functions duplicating) and it's always the same issue, it even happens when no errors are thrown like with the code example below (using index) and when errors/warnings for duplicate id's throw (by giving bad keys/id) it still happens - it always adds two more posts to the feed on refresh. Maybe I'm misunderstanding or missing something about the map function or even Next/react (this is using NextJS) so I thought I'd reach out. I just need to figure out why its duplicating but I can't for the life of me. It works as it should until I refresh. If I refresh again, it just displays the same post(s) with the same two duplicates that were added to the feed on the first refresh. Can anyone explain to me why this is happening?
import Gun from "gun/gun";
import { useReducer, useState, useEffect, useContext } from "react";
import {
Box,
Center,
Container,
VStack,
Avatar,
useColorModeValue,
Textarea,
Input
} from '#chakra-ui/react';
import { UserContext } from "../../../lib/context";
// initialize gun locally
const gun = Gun({
peers: [
'http://localhost:4000/gun'
]
})
// create the initial state to hold the messages
const initialState = {
messages: []
}
// Create a reducer that will update the messages array
function reducer(state, message) {
return {
messages: [message, ...state.messages]
}
}
export default function Feed() {
// the form state manages the form input for creating a new message
const [formState, setForm] = useState({
name: '', message: ''
})
const [user] = useContext(UserContext);
// initialize the reducer & state for holding the messages array
const [state, dispatch] = useReducer(reducer, initialState)
// when the app loads, fetch the current messages and load them into the state
// this also subscribes to new data as it changes and updates the local state
useEffect(() => {
const messages = gun.get('messages')
messages.map().on(m => {
dispatch({
userid: m.user?.issuer,
name: m.name,
message: m.message,
createdAt: m.createdAt
})
})
}, [])
// set a new message in gun, update the local state to reset the form field
function saveMessage() {
const messages = gun.get('messages')
messages.set({
userid: user?.issuer,
name: formState.name,
message: formState.message,
createdAt: Date.now()
})
setForm({
name: '', message: ''
})
}
// update the form state as the user types
function onChange(e) {
setForm({ ...formState, [e.target.name]: e.target.value })
}
return (
<div style={{ padding: 30 }}>
<input
onChange={onChange}
placeholder="Name"
name="name"
value={formState.name}
/>
<input
onChange={onChange}
placeholder="Message"
name="message"
value={formState.message}
/>
<button onClick={saveMessage}>Send Message</button>
{
state.messages.map((message, index) => (
<div key={index}>
<h4>{message.userid}</h4>
<h2>{message.message}</h2>
<h3>From: {message.name}</h3>
<p>Date: {message.createdAt}</p>
</div>
))
}
</div>
);
}
Thanks in advance for anyone who takes the time to explain this issue. I can add more pictures if needed but I'm probably going to start working on a solution without reducers and maps and then compare for now. Will update when that's done if no replies!
link to duplicate example without errors
it looks solved by changing .on back to .once. not sure why I changed that in the first place. spent 6 hours messing with map thinking that was the issue lol. mfw :O
edit: changed pretty much the whole body of this question
I am building a booking app. Tons of rerenders are happening and, following #jpmarks advice, I have tried to find out which useEffect could be the culprit, and think might be the piece of code below, which runs 24 times.
I have also uploaded all of the relevant components and console warnings to this repo, assuming that other components which render this may be affecting it.
useEffect(
() => {
console.log('useEffect ServicEstabelecimento ran') //runs 24 times
async function getOfferedServicesFromDB() {
const approved = await approvedBusinessService.doc(businessId)
const snapshotApproved = await approved.get()
if (snapshotApproved.exists && snapshotApproved.data().offeredServices) {
setOfferedServices(Object.values(snapshotApproved.data().offeredServices))
} else {
const pending = await businessPendingApprovalService.doc(businessId)
const snapshotPending = await pending.get()
if (snapshotPending.exists && snapshotPending.data().offeredServices)
setOfferedServices(Object.values(snapshotPending.data().offeredServices))
}
return
}
getOfferedServicesFromDB()
},
[
/* businessId, setOfferedServices */
],
)
//React Hook useEffect has missing dependencies:
//'businessId' and 'setOfferedServices'.
//Either include them or remove the dependency array
//Tried adding them separately or both, nothing changes
What I am trying to do here is see which services a business offers, getting that info from the database. If the business has been accepted or not yet, there's a change in how that's dealt with.
I looked through your source code and it looks like the component is not responsible for the unneccessary renders. I recommend fixing the dependencies as your editor is telling you, see if that is fixing something.
I strongly believe its your context that gets updated inside of one of your effects that causes the context to rerender the component.
To find out what state from context is causing the renders you can use the useEffect on each of them to check.
const { theme, userObject, dateForRenderingTimeGrid } = useContext(Context);
useEffect(() => {
console.log("Theme updated");
}, [theme]);
useEffect(() => {
console.log("UserObject updated");
}, [userObject]);
useEffect(() => {
console.log("DateForRenderingTimeGrid updated");
}, [dateForRenderingTimeGrid ]);
Let me know what you are seing. If none of these are actually triggering an effect but you still see the renders, then you can be certain that its happening in your component
One possibility is that in Options.js you are rendering ServiceEstabelecimento multiple times using serviceList.map. The serviceList is set in the useEffect with serviceListService.collection().onSnapshot callback. With this, naturally your useEffect will be called 24 times(if serviceList gets a value of 24 in snapshot callback)
Another possibility is that you are rendering ServiceEstabelecimento with a ternary. This will unmount/re-mount the componentthe component based on the value of estabelecimento.
const ContextOptionsEstabelecimento = React.createContext()
export const Options = () => {
const { estabelecimento, theme, offeredServices } = useContext(Context)
const [isConfiguring, setIsConfiguring] = useState(false)
const [serviceList, setServiceList] = useState([])
useEffect(() => {
const unsubscribeServices = serviceListService.collection().onSnapshot(snapshot => {
const services = snapshot.docs.map(collectIdsAndDocs)
setServiceList(services) //<-----see here
})
return () => {
unsubscribeServices()
}
}, [])
const serviceElements = serviceList.map(service => //<-----see here
!estabelecimento ? ( //<-----see here
<Service
key={service.id}
id={service.id}
name={service.name}
type={service.type}
title={service.info}
duration={service.duration}
/>
) : ( //<-----see here
<ContextOptionsEstabelecimento.Provider value={{ isConfiguring }} key={service.id}>
<ServiceEstabelecimento
key={service.id}
id={service.id}
name={service.name}
type={service.type}
title={service.info}
duration={service.duration}
/>
</ContextOptionsEstabelecimento.Provider>
),
)
return (
...
Make checks on the above and see how you go. Also share the complete repo(if possible) to debug further.
Also I see some architecting issues as I see large objects been used with multiple contexts. This will have risk of components re-rendering unnecessarily. I am assuming you are taking care of such kind of things.
So taking a look at the Apollo useMutation example in the docs https://www.apollographql.com/docs/react/data/mutations/#tracking-loading-and-error-states
function Todos() {
...
const [
updateTodo,
{ loading: mutationLoading, error: mutationError },
] = useMutation(UPDATE_TODO);
...
return data.todos.map(({ id, type }) => {
let input;
return (
<div key={id}>
<p>{type}</p>
<form
onSubmit={e => {
e.preventDefault();
updateTodo({ variables: { id, type: input.value } });
input.value = '';
}}
>
<input
ref={node => {
input = node;
}}
/>
<button type="submit">Update Todo</button>
</form>
{mutationLoading && <p>Loading...</p>}
{mutationError && <p>Error :( Please try again</p>}
</div>
);
});
}
This seems to have a major flaw (imo), updating any of the todos will show the loading state for every single todo, not just the one that has the pending mutation.
And this seems to stem from a larger problem: there's no way to track the state of multiple calls to the same mutation. So even if I did want to only show the loading state for the todos that were actually loading, there's no way to do that since we only have the concept of "is loading" not "is loading for todo X".
Besides manually tracking loading state outside of Apollo, the only decent solution I can see is splitting out a separate component, use that to render each Todo instead of having that code directly in the Todos component, and having those components each initialize their own mutation. I'm not sure if I think that's a good or bad design, but in either case it doesn't feel like I should have to change the structure of my components to accomplish this.
And this also extends to error handling. What if I update one todo, and then update another while the first update is in progress. If the first call errors, will that be visible at all in the data returned from useMutation? What about the second call?
Is there a native Apollo way to fix this? And if not, are there options for handling this that may be better than the ones I've mentioned?
Code Sandbox: https://codesandbox.io/s/v3mn68xxvy
Admittedly, the example in the docs should be rewritten to be much clearer. There's a number of other issues with it too.
The useQuery and useMutation hooks are only designed for tracking the loading, error and result state of a single operation at a time. The operation's variables might change, it might be refetched or appended onto using fetchMore, but ultimately, you're still just dealing with that one operation. You can't use a single hook to keep track of separate states of multiple operations. To do that, you need multiple hooks.
In the case of a form like this, if the input fields are known ahead of time, then you can just split the hook out into multiple ones within the same component:
const [updateA, { loading: loadingA, error: errorA }] = useMutation(YOUR_MUTATION)
const [updateB, { loading: loadingB, error: errorB }] = useMutation(YOUR_MUTATION)
const [updateC, { loading: loadingC, error: errorC }] = useMutation(YOUR_MUTATION)
If you're dealing with a variable number of fields, then we have to break out this logic into a separate because we can't declare hooks inside a loop. This is less of a limitation of the Apollo API and simply a side-effect of the magic behind hooks themselves.
const ToDo = ({ id, type }) => {
const [value, setValue] = useState('')
const options = { variables = { id, type: value } }
const const [updateTodo, { loading, error }] = useMutation(UPDATE_TODO, options)
const handleChange = event => setValue(event.target.value)
return (
<div>
<p>{type}</p>
<form onSubmit={updateTodo}>
<input
value={value}
onChange={handleChange}
/>
<button type="submit">Update Todo</button>
</form>
</div>
)
}
// back in our original component...
return data.todos.map(({ id, type }) => (
<Todo key={id} id={id} type={type] />
))
Use React for ephemeral state that doesn’t matter to the app globally
and doesn’t mutate in complex ways. For example, a toggle in some UI
element, a form input state. Use Redux for state that matters globally
or is mutated in complex ways. For example, cached users, or a post
draft.
My redux state is only representative of what has been saved to my backend database.
For my use case there is no need for any other part of the application to know about a record in an adding/editing state..
However, all my communication with my API is done through redux-thunks. I have found, getting data from redux into local state for editing is tricky.
The pattern I was trying to use:
const Container = () => {
// use redux thunk to fetch from API
useEffect(() => {
dispatch(fetchThing(id));
}, [dispatch, id]);
// get from redux store
const reduxThing = useSelector(getThing);
const save = thing => {
dispatch(saveThing(thing));
};
return (
{!fetching &&
<ThingForm
defaults={reduxThing}
submit={save}
/>}
);
};
const ThingForm = ({defaults, submit}) => {
const [values, setValues] = useState({ propA: '', propB: '', ...defaults});
const handleChange = { /*standard handleChange code here*/ };
return (
<form onSubmit={() => submit(values)}>
<input type="text" name="propA" value={values.propA} onChange={handleChange} />
<input type="text" name="propB" value={values.propB} onChange={handleChange} />
</form>
);
};
How I understand it, ThingForm is unmounted/mounted based upon "fetching." However, it is a race condition whether or not the defaults get populated. Sometimes they do, sometimes they don't.
So obviously this isn't a great pattern.
Is there an established pattern for moving data into local state from redux for editing in a form?
Or should I just put my form data into redux? (I don't know if this would be any easier).
EDIT: I think this is essentially what I am fighting: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
But no recommendation really clearly fits. I am strictly using hooks. I could overwrite with useEffect on prop change, but seems kind of messy.
EDIT:
const Container = () => {
// use redux thunk to fetch from API
useEffect(() => {
dispatch(fetchThing(id));
}, [dispatch, id]);
// get from redux store
const reduxThing = useSelector(getThing);
const save = thing => {
dispatch(saveThing(thing));
};
return (
{!fetching &&
<ThingForm
defaults={reduxThing}
submit={save}
/>}
);
};
const ThingForm = ({defaults, submit}) => {
const [values, setValues] = useState({ propA: '', propB: '', ...defaults});
const handleChange = { /*standard handleChange code here*/ };
useEffect(() => {
setValues({...values, ...defaults})
}, [defaults]);
const submitValues = (e) => {
e.preventDefault();
submit(values)
}
return (
<form onSubmit={submitValues}>
<input type="text" name="propA" value={values.propA} onChange={handleChange} />
<input type="text" name="propB" value={values.propB} onChange={handleChange} />
</form>
);
};
What you are doing is the right way, there's no reason why you should put the form data in the redux store. Like you said, "there is no need for any other part of the application to know about a record in an adding/editing state"
And that's correct.
The only problem you have is here:
{!fetching &&
<ThingForm
defaults={reduxThing}
submit={save}
/>}
Assuming fetching is true on every dispatch:
Instead of trying to hide the component (unmounting essentially), you should maybe use a spinner that overlays the page?
I don't know the rest of your code to comment on a better approach.
You also don't have to add dispatch to the dependency array
useEffect(() => {
dispatch(fetchThing(id));
}, [id]);
From the react docs:
React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.