React-Redux: Passing arguments from handler to action creator - javascript

I have scoured the web and various sources; none seem to apply to my question. The closest might be this (which doesn't have an answer):
React + Redux function call
So: I am attempting to pass arguments along to one of my action creator fields, a function called update which will determine if the blurred row had a value changed, and if so it will call my api to update. The arguments I wish to pass are the event (which contains the row I need as target.ParentElement) and an integer that represents the index of the row in my state's projects property.
Action creator in my redux store:
export const actionCreators = {
update: (e: React.FocusEvent<HTMLInputElement> | undefined, i: number): AppThunkAction<KnownAction> => (dispatch, getState) => {
let test = event;
// Will put logic and api call in here and dispatch the proper action type
}
}
And trying to call it like so:
// Inside a function rendering each row in my form
...
<input key={project.number} name={name} className='trackerCell' onBlur={(event) => { this.props.update(event, i) }} defaultValue={project.number}/>
Where i is the index value, passed to the rendering function.
This all compiles find, however when I execute and get into the update function, e and i are both undefined; event is defined though, and looks as I would expect e to look.
FWIW, the format I'm attempting here works elsewhere in my application:
requestProjects: (programNumber: number, programString: string): AppThunkAction<KnownAction> => (dispatch, getState) => {
when called by componentWillUpdate() properly receives a number and string that I am able to use in my logic.
Bonus: In all my action creator functions constructed this way, arguments has 3 objects in it: dispatch, getState and undefined. Why don't the arguments in the call signature show up? Am I thinking about these arguments differently?
And yes, I know I can just attach the index value to an attribute in my input and that will appear in the event object, but this seems hacky, and I want to actually understand what is going on here.
Thanks
UPDATE
In response to Will Cain's comment: The index variable, i, is passed to the row rendering function from it's parent, as such:
private renderProjectRow(project: ProjectTrackerState.Project, i: number) {
let cells: JSX.Element[] = [];
let someKey = project.number + '_started', name = project.number + '_number';
cells.push(<input key={project.number} name={name} className='trackerCell' onBlur={ this._handleBlur.bind(this) } defaultValue={project.number}/>);
// Rendering continues down here
It's a valid number type up to the event point (I can tell as I debug in the browser).
The event variable in the update function comes from.. I don't know where? That's a mystery I would love to solve. Even though it is not a defined parameter to the update function, when I enter the update method, event is defined as such in the debugger:
Event {isTrusted: false, type: "react-blur", target: react, currentTarget: react, eventPhase: 2, …}
It is clearly the event that triggered the handler, but how it reaches the update function is beyond me.

Answering this in case anyone comes across this same issue (unlikely, but want to be helpful):
The reason my arguments e and i were undefined at runtime is because they were not referenced in the function execution. My guess (still looking for documentation to verify) is that typescript cleans up references to unused parameters. Makes sense from an optimization standpoint. Adding a read reference to e and i inside the update function solved my issue.
You can add "noUnusedParameters": true to your compilerOptions in your tsconfig file and this will throw a typescript error for all these parameters so you can catch when these cleanups would be done.

Related

Why would you use an Action Creator callback function over using a simple object?

I'm currently learning Redux on codecademy and encountered something that feels redundant.
The course gives the following example on excessive ways to call dispatches:
store.dispatch({type:'toggle'});
store.dispatch({type:'toggle'});
store.dispatch({type:'toggle'});
In most Redux applications, action creators are used to reduce this repetition and to provide consistency. An action creator is simply a function that returns an action object with a type property. They are typically called and passed directly to the store.dispatch() method resulting in fewer errors and an easier-to-read dispatch statement.
The above code could be rewritten using an action creator called toggle() like so:
const toggle = () => {
return { type: "toggle" };
}
store.dispatch(toggle()); // Toggles the light to 'off'
store.dispatch(toggle()); // Toggles the light back to 'on'
store.dispatch(toggle()); // Toggles the light back to 'off'
My question is, why not simplify this Action Creator callback function that returns an object, by just creating an object like so?
const toggle = { type: toggle }
store.dispatch(toggle);
store.dispatch(toggle);
store.dispatch(toggle);
const toggle = Object.freeze({ type: "toggle" }) does indeed work fine and would be appropriate for this particular action. However, often an action does carry a payload, and to create actions with different payload values a function would be used, e.g.
function openSection(index) {
return {type: "open", index};
}
that you would call like
store.dispatch(openSection(1))
// or
e => store.dispatch(openSection(+e.target.dataset.index))
Also this will allow you to later add side effects to the action creator without having to refactor the code that uses the action.

How do I submit form data after another hook is set?

When a user submits a form, I have to show a modal asking them to connect if they haven't yet. On success, the modal sets the user data in a hook, and then I continue with the submission flow.
So there are 2 conditions to submit:
User submits form (clicks button)
User data is set
I solved it reactively, using an effect:
useEffect(() => {
async function nestedAsync() {
if (userData && pendingSubmitIntent) {
pendingSubmitIntent(false);
await submit(
formData, // simplified - this is actually several hooks
userData
);
}
}
nestedAsync();
}, [pendingSubmitIntent, userData]);
And the submit click handler:
setPendingSubmitIntent(true);
if (!userData) {
setShowConnectModal(true);
}
The modal is in the component:
{setShowConnectModal && (
<ConnectModal
setUserData={setUserData}
/>
)}
This actually works, but I'm getting a warning that I'm not declaring formData in the dependencies array. I can't do this, because otherwise the effect will be called when editing the inputs and that's not correct. The effect has to be called only when submitting.
And this warning makes me think that there's something fundamentally wrong with this approach. A state machine comes to mind, but I feel that it should be simpler. Any ideas?
While it may be tempting to disable the linter warning for your dependencies, I would suggest not doing that and instead look at alternatives. By disabling the linter, any future updates to your useEffect callback can cause you to miss dependencies that you might actually need to include, leading to bugs with stale values. Below are some alternatives to consider.
Removing the useEffect()
I would reconsider the need for the useEffect() hook in the first place. From the user's perspective, their form data should be saved once they submit the form, or once they click "connect" on the subsequent modal. Both of these actions are user-triggered interactions, and so the saving logic should sit within these interactions' event handlers, and not within an effect. At the moment, your effect serves the purpose of sharing shared logic between your event handlers, which is an unnecessary need for an effect as it can be achieved by creating a shared function. You can remove the effect by putting the shared logic into its own function:
function saveData(userData) {
submit(formData, userData); // note, you don't need to `await` something if you don't need the function to wait for it to complete
}
This shared function can now be used by both of your event handlers - the one that handles the submission of the form itself, and the other that handles the connection and population of your user data from within ConnectModal. By using this function, you can "continue" your submission flow from both the modal connect event and the original form submission event. This would mean updating your form submission handler to use this shared function like so:
// Submit click handler
if(userData) {
saveData(userData);
} else {
setShowConnectModal(true);
}
It also means updating your ConnectModal event handler that sets your userData to call the saveData function. This can be done by passing through a new prop to ConnectModal, eg:
<ConnectModal
onConnect={saveData}
setUserData={setUserData}
/>
Within your modal component, you can then call onConnect(...) with your userData that you call setUserData(...) with (note, as you haven't shown how userData is actually set, I assume you have an event handler in ConnectModal that controls this, the below function call to onConnect() would sit in there also):
// Connect click handler within ConnectModal
onConnect(newUserData);
Giving formData a "stable identity"
Rather than storing formData as a state value, you can look at giving it a stable identity with the useRef() hook. A stable identity means that React will always returns the same ref object on every rerender. When an object has this property, then you don't need to include it in your dependency array. Moreover, including it in the dependency array doesn't hurt either as the object isn't changing, so it won't cause the effect to execute. If formData doesn't need to influence your UI, then you can swap out its state value with a ref, allowing you to omit it as a dependency:
const formDataRef = useRef(...);
// When you need to set your formData you would use `formDataRef.current = ...`
useEffect(() => {
if (userData && pendingSubmitIntent) {
pendingSubmitIntent(false);
submit(formDataRef.current, userData); // use the ref's `.current` property which holds your formData
}
}, [pendingSubmitIntent, userData]); // no need to specify the ref as it's stable accross renders
Using the useEvent() hook
While it most likely isn't needed for your case as you can remove the effect (or use useRef()), there might be legitimate cases where you need an effect to run on a change of a particular value, but the dependency values that the linter is asking you to add will cause your effect to run too often. In such a case, you can consider using the useEvent() hook (it's currently experimental, but you can add a shim/polyfill for it if needed). For example, you could do:
const onSubmit = useEvent((userData) => {
submit(formData, userData);
});
useEffect(() => {
if (userData && pendingSubmitIntent) {
pendingSubmitIntent(false);
onSubmit(userData);
}
}, [pendingSubmitIntent, userData]);
Now the need for specifying formData within your effect is no longer needed as it's no longer used within the effect callback. The function provided to useEvent() also has access to the "latest" value of formData from the surrounding scope. Moreover, the onSubmit function returned by the useEvent() call is always the same, so it's stable. This means that it doesn't need to be provided as a dependency to the useEffect() hook call as you don't risk referring to the wrong onSubmit function. You can find more info about useEvent() here.
There are some great articles on React’s new beta documentation site relating to the above suggestions and how to best tackle your issue in different scenarios:
Removing Effect Dependencies
You Might Not Need an Effect
Separating Events from Effects
If the user already connected you can continue with the submission in the submitClickHandler.
const submitClickHandler = useCallback(()=>{
if(!userData) {
setShowConnectModal(true)
return
}
(async()=>{
if(userData && formData){
await submit(userData && formData)
}
})()
},[userData,formData])
Otherwise in the ConnectModal you will submit the form
First pass the submit and 'formData' to ConnectModal
{showConnectModal && (
<ConnectModal
setUserData={setUserData}
submit={submit}
formData={formData}
/>
)}
Then set the userData and submit the form in a useEffect hook
useEffect(()=>{
setUserData(()=>userData)
(async()=>{
if(userData && formData){
await submit(userData && formData)
}
})()
},[userData,formData])
finally close the modal

How can I reference vuex 'constant' named actions with variables?

I have an action in my vuex store like this:
[FETCH_ADAM_BROWN_LIST](state)({commit}) {
/* Action logic */
},
I want to access similarly name actions, e.g. FETCH_CHRIS_MATHISON_LIST using a variable like so:
this.$store.dispatch(`FETCH_${this.person}_LIST`);
However this throws an error:
VM90646:37 [vuex] unknown action type: FETCH_ADAM_BROWN_LIST
But it will work when I specify it a constant:
this.$store.dispatch(FETCH_ADAM_BROWN_LIST);
How can I access 'constant' named actions with variables?
Have you try using mapActions? That could work for what you want. Helpful link: How to configure Vue mapActions
I use this structure in my Vuex Store. Many of mine actions are dispatched using a concatenated string as argument. But my actions are declared like this:
setCurrentUser: function ({ dispatch, commit, getters, rootGetters }, userDB) {
commit('setCurrentUser', userDB)
dispatch('resetDashboards', userDB.dashboards)
}
Then, I can dispatch like this:
let type = 'User'
this.$store.dispatch('setCurrent' + type)
Is there something strange in your code. Remove the (state), cause actions receive a context object which in your case, you are destructuring in the params. ({commit}).
Set like this and let me know if this works.
[FETCH_ADAM_BROWN_LIST]({commit}) {
/* Action logic */
},
As you have observed, when you passed FETCH_${this.person}_LIST to dispatch, the action type that was dispatched is the concatenated constant name and not the actual value of the constant variable.
To get the actual value of the your constant variable, you can use the eval function.
From the MDN docs:
The argument of the eval() function is a string. If the string
represents an expression, eval() evaluates the expression.
You can use it like this:
const FETCH_ADAM_BROWN_LIST = 'customAction';
const person = 'ADAM_BROWN';
const actionToDispatch = `FETCH_${person}_LIST`;
console.log(eval(actionToDispatch)); // customAction
Be careful with using eval though. If you're absolutely sure your input is sanitized before passing it along, then it shouldn't be a problem.

React - Cannot read property 'target' of undefined

I'm invoking a function like below and understand this is why im getting this error. Is there a way to not invoke the function but still pass the event property?
onMouseOver={(event) => { this.moveBall(event) }}
The reason for wanting to do this is so I can do a check in the function like so:
const element = event.target ? event.target : event;
As I want to re-use this function to pass an element through on load:
// Below line is in a constructor.
this.navItem = document.querySelector('.navigation__item');
// Being called after my render
this.moveBall(this.props.navItem);
Feels like this should be doable..
I've managed to fix this with the below code but I believe that there must be a better way to achieve this:
window.addEventListener('load', () => {
const activeState = document.querySelector('.navigation__item .active')
this.moveBall(activeState)
});
** Update **
Full component code
https://jsfiddle.net/fvn1pu5r/
According to your last update all you need is just move first call to this.moveBall to react lifecycle hook componentDidMount. This ensures that DOM will have .navigation_item nodes in it. So, remove lines
window.addEventListener('load', () => {
const activeState = document.querySelector('.navigation__item .active')
this.moveBall(activeState)
});
from render method and add componentDidMount method to your class, like this:
componentDidMount() {
const activeState = document.querySelector('.navigation__item .active');
this.moveBall(activeState);
}
This should work.
Your moveBall function is being called with undefined as the argument at some stage. The event.target ? check then crashes with the error you gave.
The onMouseOver is likely always fine, as React supplies that.
Instead, I imagine it's the manual call you gave at the end. Is there a time when your this.props.navItem doesn't have a value?
Worth logging out that property right before calling the function to be sure it's always as you expect.

what's the best way to deal with undefined props in react.js?

Let's assume I have an action that gets dispatched on page load, like say, in the index.js file.
example
store.dispatch(loadData());
in my reducer I initialize state to to an object. Something like this
function myReducer(state = {}, action)
now I have some smart component that subscribes to my state and then passes it down to another component to display the data. Another important note for this scenario is the fact that the retrieval of the data is happening asynchronously.
Let's also assume that the key of this object is some array.
So the markup component would have something like this
{this.props.object.key.map(k => do something)}
Now since key is undefined, if I call map on it, I blow up. The way I have been dealing with this, is by using a simple if check. If key is defined then run .map otherwise return null. Then by the time my data gets back from the server, the render will be called again due to a change in state that this component subscribed to. At this point the key is defined and map can be called.
Another approach, Is to define what your state will look like in the reducer. In other words, if I know that my state will be an object with an array property on it, I might do something like this.
const initialState = {
key:[]
}
function myReducer(state = initialState, action)
Doing this will benefit in the fact that now I won't need my if check since key is never undefined.
My questions is; are any of these approaches better than the other? Or perhaps, is there another way entirely to deal with this?
Generally, the approach I like to take is to define default props on the component which have the semantic meaning of "empty." For example, in context of the issue you describe I would typically structure my component like this (ES6 classes style):
class MyComponent extends React.Component {
static defaultProps = {
object: {
key: []
}
};
...
method() {
// this.props.object.key is an empty array, but is overriden
// with a value later after completion of the reducer
// (trigerred asynchronously)
this.props.object.key.map(doSomething);
}
}
This is relatively clean, prevents the code from throwing at run time, and forces you to create well-defined behaviors for semantically null, empty, or undefined input states.

Categories