I want to pass props from parent to several children components , but eventually I get the error of Cannot read properties of undefined (reading 'props')
App.js :
render() {
const {currentUser} = this.props
return (
<div id="App" className='loader'>
<BrowserRouter>
<Routes>
<Route path='women' element={<Women currentUser={currentUser} />} />
Women.js:
render() {
const { isLoading } = this.state;
if (isLoading) {
return null;
}
const {currentUser} = this.props
return (
<div>
<div className='container mx-auto'>
<HomeHeaderW style={{ backgroundColor: "#fff2e0" }} currentUser={currentUser} />
HomeHeaderW.js :
function HomeHeaderW() {
const {currentUser} = this.props
const [isLogged, setLogged] = useState(false);
useEffect(() => {
if(currentUser) {
setLogged(true)
}
});
I don't know why I get the undefined error , do you have an idea about this ? Or Am I doing it in the wrong way
HomeHeaderW is a function-based component (Women and App are class-based components), you cannot get props from this.props
You should pass your props on the params
function HomeHeaderW(props) {
const {
currentUser
} = props;
const [isLogged, setLogged] = useState(false);
useEffect(() => {
if (currentUser) {
setLogged(true)
}
});
You can check this document for a better understanding
Related
while investigating a performance issue I found out that when destructuring props inside a component I am actually triggering a rerender. This does not happen if I am using props.propName or if I am destructuring the props object directly into the component paramenters:
// Rerendering
const StatefulLayout = (props) => {
const {isLoading, requestFn, initializationArray} = props
useEffect(() => {
requestFn(initializationArray);
}, [initializationArray]);
if (isLoading) {
return <PageLoading />;
}
return (
<Layout>
<React.Fragment>{children}</React.Fragment>
</Layout>
);
// Not rerendering
const StatefulLayout = ({
isLoading,
requestFn,
initializationArray
}) => {
useEffect(() => {
requestFn(initializationArray);
}, [initializationArray]);
if (isLoading) {
return <PageLoading />;
}
return (
<Layout>
<React.Fragment>{children}</React.Fragment>
</Layout>
);
What this happens? What would be the correct pattern?
Can we pass hook as a function to a component and use it in that component?
Like in the example below I am passing useProps to withPropConnector which returns a Connect component. This useProps is being used in the Connect component. Am I violating any hook rules?
// Landing.jsx
export const Landing = (props) => {
const { isTypeOne, hasFetchedData } = props;
if (!hasFetchedData) {
return <Loader />;
}
const renderView = () => {
if (isSomeTypeOne) {
return <TypeOneView />;
}
return <TypeTwoView />;
};
return (
<>
<Wrapper>
{renderView()}
<SomeNavigation />
</Wrapper>
<SomeModals />
</>
);
};
const useProps = () => {
const { query } = useRouter();
const { UID } = query;
const { isTypeOne, isTypeTwo } = useSelectorWithShallowEqual(getType);
const hasFetchedData = useSelectorWithShallowEqual(
getHasFetchedData(UID)
);
const props = {
isTypeOne,
isTypeTwo,
hasFetchedData
};
return props;
};
export default withPropConnector(useProps, Landing);
// withPropConnector.js
const withPropConnector = (useProps, Component) => {
const Connect = (propsFromParent = emptyObject) => {
const props = useProps(propsFromParent);
return <Component {...propsFromParent} {...props} />;
};
return Connect;
};
export default withPropConnector;
I have a parent component GoalList which maps to a child component:
{data.goals.map((item, index) => {
return (
<Link
href={{ pathname: "/goal", query: { id: item.id } }}
key={`goal-item-${index}`}
>
<a>
<li>
<div>{item.title}</div>
</li>
</a>
</Link>
);
})}
next/router's page:
import SingleGoal from "../components/SingleGoal";
const Single = () => {
return <SingleGoal />;
};
export default Single;
Child Component:
const SingleGoal = () => {
const [id, setId] = useState("");
const router = useRouter();
useEffect(() => {
if (router.query.id !== "") setId(router.query.id);
}, [router]);
const { loading, error, data } = useQuery(SINGLE_GOAL_QUERY, {
variables: { id: id },
});
if (loading) return <p>Loading...</p>;
if (error) return `Error! ${error.message}`;
return (
<div>
<h1>{data.goal.title}</h1>
<p>{data.goal.endDate}</p>
</div>
);
};
When I click on Link in the parent component, the item.id is properly transferred and the SINGLE_GOAL_QUERY executes correctly.
BUT, when I refresh the SingleGoal component, the router object takes a split second to populate, and I get a GraphQL warning:
[GraphQL error]: Message: Variable "$id" of required type "ID!" was not provided., Location: [object Object], Path: undefined
On a similar project I had previously given props to next/router's page component, but this no longer seems to work:
const Single = (props) => {
return <SingleGoal id={props.query.id} />;
};
How do I account for the delay in the router object? Is this a situation in which to use getInitialProps?
Thank you for any direction.
You can set the initial state inside your component with the router query id by reordering your hooks
const SingleGoal = () => {
const router = useRouter();
const [id, setId] = useState(router.query.id);
useEffect(() => {
if (router.query.id !== "") setId(router.query.id);
}, [router]);
const { loading, error, data } = useQuery(SINGLE_GOAL_QUERY, {
variables: { id: id },
});
if (loading) return <p>Loading...</p>;
if (error) return `Error! ${error.message}`;
return (
<div>
<h1>{data.goal.title}</h1>
<p>{data.goal.endDate}</p>
</div>
);
};
In this case, the secret to props being transferred through via the page was to enable getInitialProps via a custom _app.
Before:
const MyApp = ({ Component, apollo, pageProps }) => {
return (
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} />
</Page>
</ApolloProvider>
);
};
After:
const MyApp = ({ Component, apollo, pageProps }) => {
return (
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} />
</Page>
</ApolloProvider>
);
};
MyApp.getInitialProps = async ({ Component, ctx }) => {
let pageProps = {};
if (Component.getInitialProps) {
// calls page's `getInitialProps` and fills `appProps.pageProps`
pageProps = await Component.getInitialProps(ctx);
}
// exposes the query to the user
pageProps.query = ctx.query;
return { pageProps };
};
The only downfall now is that there is no more static page generation, and server-side-rendering is used on each request.
I have this child component called TodoList
const TodoItem = ({ checked, children }) =>
(<TouchableOpacity
style={{ backgroundColor: checked && 'red'}}>
{children}
</TouchableOpacity>
);
const TodoList = props => {
const {
options = [],
onSelect,
...rest
} = props;
const [selectedOptionIndex, setSelectedOptionIndex] = useState(null);
useEffect(() => {
onSelect(options[selectedOptionIndex]);
}, [onSelect, options, selectedOptionIndex]);
const renderItem = (o, index) => {
return (
<TodoItem
key={o + index}
onPress={() => setSelectedOptionIndex(index)}
checked={index === selectedOptionIndex}>
{index === selectedOptionIndex && <Tick />}
<Text>{o}</Text>
</TodoItem>
);
};
return (
<View {...rest}>{options.map(renderItem)}</View>
);
};
export default TodoList;
And I have a parent component called Container
export default function() {
const [item, setItem] = setState(null);
return (
<Screen>
<TodoList options={[1,2,3]} onSelect={(i) => setItem(i)} />
</Screen>
)
}
I want to have a callback from child component to parent component using onSelect whenever a TodoItem is selected. However, whenever the onSelect is called, my TodoList re-renders and my selectedOptionIndex is reset. Hence, my checked flag will only change to true briefly before resetting to false.
If I remove the onSelect callback, it works fine. But I need to setState for both child and parent. How do I do that?
It's hard to tell why thats happening for you, most likely because the container's state is changing, causing everything to rerender.
Something like this should help you out, though.
const { render } = ReactDOM;
const { useEffect, useState } = React;
const ToDoItem = ({checked, label, onChange, style}) => {
const handleChange = event => onChange(event);
return (
<div style={style}>
<input type="checkbox" checked={checked} onChange={handleChange}/>
{label}
</div>
);
}
const ToDoList = ({items, onChosen}) => {
const [selected, setSelected] = useState([]);
const handleChange = item => event => {
let s = [...selected];
s.includes(item) ? s.splice(s.indexOf(item), 1) : s.push(item);
setSelected(s);
onChosen(s);
}
return (
<div>
{items && items.map(i => {
let s = selected.includes(i);
return (
<ToDoItem
key={i}
label={i}
onChange={handleChange(i)}
checked={s}
style={{textDecoration: s ? 'line-through' : ''}}/>
)
})}
</div>
);
}
const App = () => {
const [chosen, setChosen] = useState();
const handleChosen = choices => {
setChosen(choices);
}
return (
<div>
<ToDoList items={["Rock", "Paper", "Scissors"]} onChosen={handleChosen} />
{chosen && chosen.length > 0 && <pre>Chosen: {JSON.stringify(chosen,null,2)}</pre>}
</div>
);
}
render(<App />, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
Turned out my top-level component Screen is causing this re-render. In my Screen functional component, I have this piece of code before the return
const Content = scroll
? contentProps => {
const { style: contentContainerStyle } = contentProps;
return (
<ScrollView {...contentContainerStyle}>
{contentProps.children}
</ScrollView>
);
}
: View;
return (
<Content>{children}</Content>
)
And it somehow (not sure why) causes the children to re-render every time my state changes.
I fixed it by removing the function and have it simply returning a View
const Content = scroll ? ScrollView : View;
Is it not allowed to use hooks inside of a Higher Order Component? When I try to do it with this simple pattern I'm getting the error Invalid hook call. Hooks can only be called inside of the body of a function component.
// App.js
import React, { useState } from 'react';
const WithState = (Component) => {
const [state, dispatch] = useState(0);
return () => <Component state={state} dispatch={dispatch} />;
}
const Counter = ({ state }) => {
return (
<div style={{ textAlign: 'center', margin: '0 auto'}}>
{state}
</div>
)
}
const CounterWithState = WithState(Counter);
const App = () => {
return <CounterWithState />;
}
export default App;
I believe you should use the hooks inside the HOC:
const WithState = (Component) => {
const WithStateComponent = () => {
const [state, dispatch] = useState(0);
return <Component state={state} dispatch={dispatch} />;
}
return WithStateComponent;
}
Inspired by Rafael Souza's answer, you can make it even cleaner with:
const WithState = (Component) => {
return () => {
const [state, dispatch] = useState(0);
return <Component state={state} dispatch={dispatch} />
}
}