I want to communicate from my Child component (a input form) to my parent component (popup) weather or not there is any data in the child.
The issue im facing is that the child component isn't a set child in the code it gets there with the {props.children} tag:
App.js structure:
<div>
<Popup>
</Child>
</Popup>
</div>
Popup.js structure:
<div>
{this.props.children}
</div>
Is there a way to do this without using a window.* variable or frankenstein-ing a stateSet/stateRead function in my App.js?
I have done some risky stuff here, but it gets the job done:
import React, { useEffect, useState } from "react";
export default function NewApp() {
return (
<Parent pProps="boss">
<Child text="Hello1" />
<Child text="Hello2" />
<Child text="Hello3" />
</Parent>
);
}
function Parent(props) {
const [children, setChildren] = useState([]);
function communicateWithMe(val) {
console.log("I am called", val);
}
useEffect(() => {
let _children = React.Children.map(props.children, (child) => {
console.log("Parent child", child);
return {
...child,
props: {
...child.props,
callBack: communicateWithMe
}
};
});
console.log("_children", _children);
setChildren(_children);
}, []);
return (
<div
style={{
background: "black",
color: "white"
}}
>
{children}
</div>
);
}
function Child(props) {
console.log(props);
return (
<div>
<p>{props.text}</p>
{props.callBack && (
<button
onClick={() => {
props.callBack("children baby");
}}
>
invoke Parent function
</button>
)}
</div>
);
}
here is the sandbox version to see it in action:
https://codesandbox.io/s/sad-wood-5kuit3?file=/NewApp.js
Explaination:
What I aimed to do was to append the props on the children from the parent component. To do that, I casted the children (received in props by Parent) in to local state.
Appended the prop to each child to have a callback function to communicate with the parent.
The Parent component now returns the state variable that has the modified / appended props, instead of returns prop.children as it received!
EDIT:
As suggested, I have used React.Children to iterate over the children recieved by the parent in props.
I have a function that I declared on a file, like this
export default function doSomething (param1, param2, param3) {
*Do something here*
}
And then I want to use that function whenever I press a button on a screen I have declared in another file:
import doSomething from '../../doSomething';
export default function Screen(props) {
return (
<Container>
<SafeAreaProvider>
<Button onPress={() => doSomething(1, 2, 3)} />
</SafeAreaProvider>
</Container>
);
}
However, any time I press the button, it gives me a Invalid hook call. Hooks can only be called inside of the body of a function component error. I'm fairly new to custom hooks/external functions, so how can I resolve is?
If doSomething is a hook and not a plain JS function, then you can not just import it and call it as you would do with plain functions.
The following code fixes the issue.
export default function useDoSomething() {
...
const doSomething = React.useCallback((parameter1, parameter2, parameter3) => {}, [])
return {
doSomething,
}
}
Then use the hook as follows.
const { doSomething } = useDoSomething()
return (
<Container>
<SafeAreaProvider>
<Button onPress={() => doSomething(1, 2, 3)} />
</SafeAreaProvider>
</Container>
);
My React component uses apollo to fetch data via graphql
class PopUpForm extends React.Component {
constructor () {
super()
this.state = {
shoptitle: "UpdateMe",
popupbodyDesc: "UpdateMe"
}
}
render()
{
return (
<>
<Query query={STORE_META}>
{({ data, loading, error, refetch }) => {
if (loading) return <div>Loading…</div>;
if (error) return <div>{error.message}</div>;
if (!data) return (
<p>Could not find metafields :(</p>
);
console.log(data);
//loop over data
var loopedmetafields = data.shop.metafields.edges
console.log(loopedmetafields)
loopedmetafields.forEach(element => {
console.log(element.node.value)
if (element.node.value === "ExtraShopDescription"){
this.setState({
shoptitle: element.node.value
});
console.log(this.state.shoptitle)
}
if (element.node.value === "bodyDesc"){
this.setState({
popupbodyDesc: element.node.value
});
console.log(this.state.popupbodyDesc)
}
});
return (
<>
<AddTodo mkey="ExtraShopDesc" namespace="ExtraShopDescription" desc={this.state.shoptitle} onUpdate={refetch} />
<AddTodo mkey="body" namespace="bodyDesc" desc={this.state.popupbodyDesc} onUpdate={refetch} />
</>
);
}}
</Query>
</>
)
}
}
export default PopUpForm
Frustratingly the functional component renders before the state is set from the query. Ideally the functional component would only render after this as I thought was baked into the apollo library but seems I was mistaken and it seems to execute synchronous rather than asynchronous
As you can see I pass the props to the child component, in the child component I use these to show the current value that someone might amend
The functional component is here
function AddTodo(props) {
let input;
const [desc, setDesc] = useState(props.desc);
//console.log(desc)
useEffect( () => {
console.log('props updated');
console.log(props)
}, [props.desc])
const [addTodo, { data, loading, error }] = useMutation(UPDATE_TEXT, {
refetchQueries: [
'STORE_META' // Query name
],
});
//console.log(data)
if (loading) return 'Submitting...';
if (error) return `Submission error! ${error.message}`;
return (
<div>
<form
onSubmit={e => {
console.log(input.value)
setDesc(input.value)
e.preventDefault();
const newmetafields = {
key: props.mkey,
namespace: props.namespace,
ownerId: "gid://shopify/Shop/55595073672",
type: "single_line_text_field",
value: input.value
}
addTodo({ variables: { metafields: newmetafields } });
input.value = input.value
}}
>
<p>This field denotes the title of your pop-up</p>
<input className="titleInput" defaultValue={desc}
ref={node => {
input = node;
}}
/>
<button className="buttonClick" type="submit">Update</button>
</form>
</div>
);
}
Now I need this component to update when the setState is called on PopUpForm
Another stack overflow answer here gives me some clues
Passing the intial state to a component as a prop is an anti-pattern
because the getInitialState (in our case the constuctor) method is
only called the first time the component renders. Never more. Meaning
that, if you re-render that component passing a different value as a
prop, the component will not react accordingly, because the component
will keep the state from the first time it was rendered. It's very
error prone.
Hence why I then implemented useEffect however the console.log in useEffect is still "updateMe" and not the value as returned from the graphql call.
So where I'm at
I need the render the functional component after the the grapql call
and I've manipulated the data, this seems to be the best approach in terms of design patterns also
or
I need setState to pass/render the functional component with the new value
As an aside if I do this
<AddTodo mkey="ExtraShopDesc" namespace="ExtraShopDescription" desc={data.shop.metafields.edges[0].node.value} onUpdate={refetch} />
It will work but I can't always expect the value to be 0 or 1 as metafields might have already defined
I think there is a simpler way than using setState to solve this. You can for example use find like this:
const shopTitleElement = loopedmetafields.find(element => {
return element.node.value === "ExtraShopDescription"
})
const shopBodyElement = loopedmetafields.find(element => {
return element.node.value === "bodyDesc"
});
return (
<>
<AddTodo mkey="ExtraShopDesc" namespace="ExtraShopDescription" desc={shopTitleElement.node.value} onUpdate={refetch} />
<AddTodo mkey="body" namespace="bodyDesc" desc={shopBodyElement.node.value} onUpdate={refetch} />
</>
);
I'm using a HOC component to bind an action to many different types of element, including SVG cells, which, when an onClick is bound normally, it works, but when I use my HOC it returns un-intended results.
Minimally reproducible example: https://codesandbox.io/s/ecstatic-keldysh-3viw0
The HOC component:
export const withReport = Component => ({ children, ...props }) => {
console.log(Component); //this only prints for ListItem elements for some reason
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
console.log('clicked!'); //even this wont work on some.
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return (
<Component onClick={handleClick} {...props}>
{children}
</Component>
);
};
Usage working:
const ListItemWIthReport = withReport(ListItem); //list item from react-mui
{items.map((item, key) => (
<ListItemWithReport report={item.report} key={key} button>
{/* listitem children*/}
</ListItemWithReport>
))}
Usage not working:
const BarWithReport = withReport(Bar); //Bar from recharts
{bars.map((bar, index) => (
<BarWithReport
report={bar.report}
key={index}
dataKey={bar.name}
fill={bar.fill}
/>
))}
The ListItem works 100% as anticipated, however, the bars will not render inside of the BarChart. Similarly, with a PieChart the Cells will actually render, with the correct sizes according to their values, however, props like "fill" do not appear to pass down.
Am I using the HOC incorrectly? I don't see an option other than HOC for the inside of Charts as many types of elements will be considered invalid HTML?
You might be dealing with components that have important static properties that need to be hoisted into the wrapped component or need to have ref forwarding implemented in order for their parent components to handle them. Getting these pieces in place is important, especially when wrapping components where you don't know their internals. That Bar component, for example, does have some static properties. Your HOC is making those disappear.
Here's how you can hoist these static members:
import hoistNonReactStatic from 'hoist-non-react-statics';
export const withReport = Component => {
const EnhancedComponent = props => {
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return (
<Component onClick={handleClick} {...props}/>
);
};
hoistNonReactStatic(EnhancedComponent, Component);
return EnhancedComponent;
};
Docs on hoisting statics and ref forwarding can be found in this handy guide to HOCs.
There may be some libraries that can take care of all these details for you. One, addhoc, works like this:
import addHOC from 'addhoc';
export const withReport = addHOC(render => {
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return render({ onClick: handleClick });
});
Of course, if the parent component is checking child components by type explicitly, then you won't be able to use HOCs at all. In fact, it looks like recharts has that issue. Here you can see the chart is defined in terms of child components which are then searched for explicitly by type.
I think your HOC is invalid, because not every wrapper-Component (e.g. HTML element) is basically clickable. Maybe this snipped can clarify what I am trying to say:
const withReport = Component => (props) => {
const handleClick = () => console.log('whatever')
// Careful - your component might not support onClick by default
return <Component onClick={handleClick} {...props} />
// vs.
return <div onClick={handleClick} style={{backgroundColor: 'green'}}>
<Component {...props} />
{props.children}
</div>
}
// Your import from wherever you want
class SomeClass extends React.Component {
render() {
return <span onClick={this.props.onClick}>{this.props.children}</span>
// vs.
return <span style={{backgroundColor: 'red'}}>
{
// Careful - your imported component might not support children by default
this.props.children
}
</span>
}
}
const ReportedListItem = withReport(SomeClass)
ReactDOM.render(<ReportedListItem>
<h2>child</h2>
</ReportedListItem>, mountNode)
You can have the uppers or the unders (separated by vs.) but not crossed. The HOC using the second return (controlled wrapper-Component) is sure more save.
I've used 4 methods successfully to wrap Recharts components.
First Method
Wrap the component in a HOC and use Object.Assign with some overloads. This breaks some animation and difficult to use an active Dot on lines. Recharts grabs some props from components before rendering them. So if the prop isn't passed into the HOC, then it won't render properly.
...
function LineWrapper({
dataOverload,
data,
children,
strokeWidth,
strokeWidthOverload,
isAnimationActive,
dot,
dotOverload,
activeDot,
activeDotOverload,
...rest
}: PropsWithChildren<Props>) {
const defaultDotStroke = 12;
return (
<Line
aria-label="chart-line"
isAnimationActive={false}
strokeWidth={strokeWidthOverload ?? 2}
data={dataOverload?.chartData ?? data}
dot={dotOverload ?? { strokeWidth: defaultDotStroke }}
activeDot={activeDotOverload ?? { strokeWidth: defaultDotStroke + 2 }}
{...rest}
>
{children}
</Line>
);
}
export default renderChartWrapper(Line, LineWrapper, {
activeDot: <Dot r={14} />,
});
const renderChartWrapper = <P extends BP, BP = {}>(
component: React.ComponentType<BP>,
wrapperFC: React.FC<P>,
defaultProps?: Partial<P>
): React.FC<P> => {
Object.assign(wrapperFC, component);
if (defaultProps) {
wrapperFC.defaultProps = wrapperFC.defaultProps ?? {};
Object.assign(wrapperFC.defaultProps, defaultProps);
}
return wrapperFC;
};
Second Method
Use default props to assign values. Any props passed into the HOC will be overridden.
import { XAxisProps } from 'recharts';
import { createStyles } from '#material-ui/core';
import { themeExtensions } from '../../../assets/theme';
const useStyles = createStyles({
tickStyle: {
...themeExtensions.font.graphAxis,
},
});
type Props = XAxisProps;
// There is no actual implementation of XAxis. Recharts render function grabs the props only.
function XAxisWrapper(props: Props) {
return null;
}
XAxisWrapper.displayName = 'XAxis';
XAxisWrapper.defaultProps = {
allowDecimals: true,
hide: false,
orientation: 'bottom',
width: 0,
height: 30,
mirror: false,
xAxisId: 0,
type: 'category',
domain: [0, 'auto'],
padding: { left: 0, right: 0 },
allowDataOverflow: false,
scale: 'auto',
reversed: false,
allowDuplicatedCategory: false,
tick: { style: useStyles.tickStyle },
tickCount: 5,
tickLine: false,
dataKey: 'key',
};
export default XAxisWrapper;
Third Method
I didn't like this so I've worked around it, but you can extend the class.
export default class LineWrapper extends Line {
render(){
return (<Line {...this.props} />
}
}
Fourth Method
I don't have a quick example of this, but I always render the shape or children and provide functions to help. For example, for bar cells I use this:
export default function renderBarCellPattern(cellOptions: CellRenderOptions) {
const { data, fill, match, pattern } = cellOptions;
const id = _uniqueId();
const cells = data.map((d) =>
match(d) ? (
<Cell
key={`cell-${id}`}
strokeWidth={4}
stroke={fill}
fill={`url(#bar-mask-pattern-${id})`}
/>
) : (
<Cell key={`cell-${id}`} strokeWidth={2} fill={fill} />
)
);
return !pattern
? cells
: cells.concat(
<CloneElement<MaskProps>
key={`pattern-${id}`}
element={pattern}
id={`bar-mask-pattern-${id}`}
fill={fill}
/>
);
}
// and
<Bar {...requiredProps}>
{renderBarCellPattern(...cell details)}
</Bar>
CloneElement is just a personal wrapper for Reacts cloneElement().
How do I make sure I set a value in the context provider before components are mounted?
In the code example below, the console.log in the child component(Dashboard) will be logged first (as undefined). Why is that and is there any way for me to make sure the value is set before that component is mounted?
App.js
render() {
return (
<div className='App'>
<ContextProvider>
<Dashboard />
</ContextProvider>
</div>
);
}
ContextProvider.js
componentDidMount = () => {
this.setState({value: 1})
console.log(this.state.value);
}
Dashboard.js
componentDidMount = () => {
console.log(this.context.value);
}
Children are rendered first. Regardless of that, setState is asynchronous, so a context will be provided to consumers asynchronously.
In case there's a necessity for children to wait for a context, they should be either conditionally rendered:
render() {
this.context.value && ...
}
Or be wrapped with context consumer which can be written as a HOC for reuse:
const withFoo = Comp => props => (
<FooContext.Consumer>{foo => foo.value && <Comp {...props}/>}</FooContext.Consumer>
);