I was wondering if i could wrap dispatch action into function (for example class method). I have this component:
function Product({id}) {
const {state, dispatch} = React.useContext(CartContext);
return (
<button onClick={() => dispatch({type: "remove", payload: id})}>Remove</button>
)
}
What i want to achieve is to replace ugly looking dispatch call into more clear function like this:
<button onClick={() => Cart.remove(id))}>Remove</button>
Is it possible? I've tried by this way but hooks can't be called outside React component.
export default Cart {
static remove = id => React.useContext(CartContext).dispatch({type: "remove", payload: id});
}
What you need is to create a custom hook
const useRemoveCart = () => {
const {state, dispatch} = React.useContext(CartContext);
return id => dispatch({type: "remove", payload: id})
}
And now you can use this hook and call the return of it.
function Product({id}) {
const remove = useRemoveCart()
return (
<button onClick={() => remove(id)}>Remove</button>
)
}
But I don't feel like this is the way to go.
Probably the max thing you could do is create a useCart hook that will return state and dispatch. Creating a custom hook only for one function isn't good, because if you need another function, you will have to do a lot of refactor or create a new hook, and you will have one hook for each function, which will be very bad.
If I was you, I would do this
const useCart = () => React.useContext(CartContext)
Now you don't need to import useContext and CartContext, only import useCart
And probably create variables instead of passing the hole string "remove" which can cause some typos.
const REMOVE_CART = 'remove'
And use it like
dispatch({type: REMOVE_CART, payload: id})
Now you will never have a typo in the 'remove' string because if you do, it will give you an error.
You shouldn't pass dispatch to child components. Child components should typically pass the data back up to the parent, and the parent should solely be responsible for the state in the case. I'd suggest a Helper function in Product that does this.
Related
I am constructing some node objects in a function(prepareNodes) to pass to React Flow within a functional component A (lets say), and I have defined a custom node component(CardNode) stateless, which has a button. On button click it should trigger the function(prepareNodes) defined within Component A.
function ComponentA = ({ selectedNodes }) => {
const reactFlowWrapper = useRef(null);
const [elements, setElements] = useState([]);
const [edges, setEdges] = useState([]);
const prepareNode = async (nodeid) => {
//some service calls to fetch data and constuct nodes
setElements([ ...nodes]);
setEdges([...edges]);
}
return (
<ReactFlowProvider>
<div className="reactflow-wrapper" ref={reactFlowWrapper}>
<ReactFlow
nodes={elements}
edges={edges}
//some properties
>
</ReactFlow>
</div>
</ReactFlowProvider>
)
};
export default ComponentA;
function CardNode({ data }) {
const renderSubFlowNodes = (id) => {
console.log(id);
//prepareNode(id)
}
return (
<>
<Handle type="target" position={Position.Top} />
<div className="flex node-wrapper">
<button className="btn-transparent btn-toggle-node" href="#" onClick={() => renderSubFlowNodes(data['id']) }>
<div>
<img src={Icon}/>
</div>
</button>
</div>
<Handle type="source" position={Position.Bottom}/>
</>
);
}
export default CardNode;
I looked for some references online, and most of them suggest to move this resuable function out of the component, but since this function carries a state that it directly sets to the ReactFlow using useState hook, I dont think it would be much of a help.
Other references talks about using useCallback or useRefs and forwardRef, useImperativeHandle especially for functional component, Which I did not quite understand well.
Can someone suggest me a solution or a work around for this specific use-case of mine.
You can add an onClick handler to the each node, and within the node view you call this handler on click.
In the parent Component within the onClick handler you can call prepareNode as needed.
useEffect(() => {
setElements(
elements.map(item => {
...item,
onClick: (i) => {
console.log(i);
prepareNode();
},
})
)},
[]);
The classical approach is to have a parent object that defines prepareNode (along with the state items it uses) and pass the required pieces as props into the components that use them.
That "parent object" could be a common-ancestor component, or a Context (if the chain from the parent to the children makes it cumbersome to pass the props all the way down it).
, Using props I was able to effectively pass state upwards from my child component to its parent, but a change in the state does not cause a re-render of the page.
import React, { useState } from "react";
export default function App() {
const AddToList = (item) => {
setText([...text, item]);
};
const removeFromList = (item) => {
const index = text.indexOf(item);
setText(text.splice(index, 1));
};
const [text, setText] = React.useState(["default", "default1", "default2"]);
return (
<div className="App">
<div>
<button
onClick={() => {
AddToList("hello");
}}
>
Add
</button>
</div>
{text.map((item) => {
return <ChildComponent text={item} removeText={removeFromList} />;
})}
</div>
);
}
const ChildComponent = ({ text, removeText }) => {
return (
<div>
<p>{text}</p>
<button
onClick={() => {
removeText(text);
}}
>
Remove
</button>
</div>
);
};
In the snippet, each time AddToList is called, a new child component is created and the page is re-rendered reflecting that. However, when i call removeFromList on the child component, nothing happens. The page stays the same, even though I thought this would reduce the number of childComponents present on the page. This is the problem I'm facing.
Updated Answer (Following Edits To Original Question)
In light of your edits, the problem is that you are mutating and passing the original array in state back into itself-- React sees that it is receiving a reference to the same object, and does not update. Instead, spread text into a new duplicate array, splice the duplicate array, and pass that into setText:
const removeFromList = (item) => {
const index = text.indexOf(item);
const dupeArray = [...text];
dupeArray.splice(index, 1);
setText(dupeArray);
};
You can see this working in this fiddle
Original Answer
The reason React has things like state hooks is that you leverage them in order to plug into and trigger the React lifecycle. Your problem does not actually have anything to do with a child attempting to update state at a parent. It is that while your AddToList function is properly leveraging React state management:
const AddToList = (item) => {
setText([...text, item]);
};
Your removeFromList function does not use any state hooks:
const removeFromList = (item) => {
const index = text.indexOf(item);
text.splice(index, 1); // you never actually setText...
};
...so React has no idea that state has updated. You should rewrite it as something like:
const removeFromList = (item) => {
const index = text.indexOf(item);
const newList = text.splice(index, 1);
setText(newList);
};
(Also, for what it is worth, you are being a little loose with styles-- your AddToList is all caps using PascalCase while removeFromCase is using camelCase. Typically in JS we reserve PascalCase for classes, and in React we also might leverage it for components and services; we generally would not use it for a method or a variable.)
Well, i have this error
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
I tried alot of different options to fix this, but i failed.
Here is my code
export const DataInput = () => {
const Post = (testTitle, testText) => {
useFirestore().collection('schedule-data').doc('test').set({
testTitle: testTitle,
testText: testText
})
}
return(
<Button
variant="primary"
onClick={()=> Post(testTitle, testText)}>
POST data
</Button>
Deleted some of code that does not matter
Hooks can only be called while rendering a component, so they need to be in the body of your component.
export const DataInput = () => {
const firestore = useFirestore();
const Post = (testTitle, testText) => {
firestore.collection('schedule-data').doc('test').set({
testTitle: testTitle,
testText: testText
})
}
// etc
}
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, explanation available here)
According to you code samle I may suggest that testTitle, testText available in DataInput in some way, thus you may create onClick handler with useCallback. React will create callback for use as handler, and re-create only when testTitle, testText changed.
import {useCallback} from 'react';
export const DataInput = () => {
const makePost = useCallback(() => {
useFirestore().collection('schedule-data').doc('test').set({
testTitle: testTitle,
testText: testText
})
}, [testTitle, testText]);
return (
<Button
variant="primary"
onClick={makePost}
{/* Avoid inline callback declaration */}
>
POST data
</Button>
)
}
I'm using useDispatch hook (from Redux) in onSelect callback in the Tree component (from Ant library):
export const MyComponent = () => {
const dispatch = useDispatch();
const onSelect = (selectedNode) => {
const selectedItem = selectedNode[0];
dispatch(fetchSelectedItems(selectedItem));
};
return
<Tree
onSelect={onSelect}
>
<TreeNode .. />
<TreeNode .. />
<TreeNode .. />
</Tree
}
export const fetchSelectedItems = (selected: string) =>
(dispatch) =>
axios({
url: `/api/items?id=${selected}`,
method: 'GET',
}).then(response => {
dispatch(fetchSelectedItemsSuccess(response.data))
}).catch((error: any) => {throw(error)});
Why does useDispatch re-render parent components? Is there any way to prevent from this? I tried useCallback like it's in Redux documentation but this solution is to prevent child components from re-rendering, not parents.
It looks like my assumption in the comment was correct.
So I will show you the workaround.
You can extract the part that uses clickValue in the container to another component, say ClickValue.
Doing so will isolate the update to ClickValue component only.
My fork: https://codesandbox.io/s/soanswer60515755-9cc7u
function ClickValue() {
const clickValue = useSelector(state => state.value);
console.log(clickValue);
return clickValue;
}
export default function Container() {
return (
<div className="Container">
<h3>Container</h3>
<ParentComponent />
<ClickValue />
</div>
);
}
Check out the profile result below.
I would think that on every render you are redeclaring the onSelect function. Functions are reference types. Passing that redeclared function with its new reference on ever render will cause a rerender. Perhaps you should look into using context.
My problem with re-rendering components was caused by useSelector used in parent components where I directly refer to state. Most probably because of new result of this selector..
Solution:
I rewrote this selectors with reselect library to make them memoized (it was suggested in one of comments here but I don't know why its've been removed). I did exactly what is in redux documentation about memoized selectors.
I need to pass props to selectors so that i can fetch the content of the clicked item from the selectors. However i could not pass the props. I tried this way but no success
const mapStateToProps = createStructuredSelector({
features: selectFeatures(),
getFeatureToEditById: selectFeatureToEditById(),
});
handleFeatureEdit = (event, feature) => {
event.preventDefault();
console.log("feature handle", feature);
const dialog = (
<FeatureEditDialog
feature={feature}
featureToEdit={selectFeatureToEditById(feature)}
onClose={() => this.props.hideDialog(null)}
/>
);
this.props.showDialog(dialog);
};
selectors.js
const selectFeatureState = state => state.get("featureReducer");
const selectFeatureById = (_, props) => {
console.log("props", _, props); #if i get the id of feature here
# i could then filter based on that id from below selector and show
# the result in FeatureEditDialog component
};
const selectFeatureToEditById = () =>
createSelector(
selectFeatureState,
selectFeatureById,
(features, featureId) => {
console.log("features", features, featureId);
}
);
Here is the gist for full code
https://gist.github.com/MilanRgm/80fe18e3f25993a27dfd0bbd0ede3c20
Simply pass both state and props from your mapStateToProps to your selectors.
If you use a selector directly as the mapStateToProps function, it will receive the same arguments mapState does: state and ownProps (props set on the connected component).
A simple example:
// simple selector
export const getSomethingFromState = (state, { id }) => state.stuff[id]
// reselect selector
export const getStuff = createSelector(
getSomethingFromState,
(stuff) => stuff
)
// use it as mapDispatchToProps
const mapDispatchToProps = getSomethingFromState
const MyContainer = connect(mapDispatchToProps)(MyComponent)
// and set id as an own prop in the container when rendering
<MyContainer id='foo' />
However you're doing some weird things like mapping a selector to reuse it later. It doesn't work that way, at least it's not intended to be used that way.
You use selectors to retrieve slices of your state and pass it as props to your connected components. Whenever the state changes, your selectors will be re-run (with some caching thanks to reselect). If something the component is actually retrieving from Redux has changed indeed, it will re-render.
So your FeatureEditDialog component should be connected as well, and should be capable of retrieving anything it needs from the Redux state, just by using props (which feature, which id, so on) in its own connect call.
this.props.showDialog(dialog); is a big code smell as well. ;)