Redux toolkit watching state - javascript

I have component which represent page, therefore it is parent Component.
I also have a second component which represents table, therefore it is child Component.
In parent component I skip api call, but I want watch data, because if there's no data, I don't want display filters, etc.
const Page = () => {
const { isUninitialized, data } = useGetUsersQuery({ skip: true})
if (!isUninitialized && !data.length) return <div>There's no data.</div>
return <>
<Filters />
<Table />
</>
}
const Table = () => {
const { isLoading, isFetching, data } = useGetUsersQuery()
if (isLoading || isFetching) return <div>Loading...</div>
if (data) return <table>...</table>
return null
}
For some reason it doesn't work.
I thought of using a useSelector hook.
const Page = () => {
const { isUninitialized, data } = useSelector("???")
if (!isUninitialized && !data?.length) return <div>There's no data.</div>
return <>
<Filters />
<Table />
</>
}
But it's not look much effective.
Till now I used combination of setState and useEffect.
const Page = () => {
cont [ noData, setNoData ] = useState(false)
if (noData) return <div>There's no data.</div>
return <>
<Filters />
<Table setNoData={setNoData}/>
</>
}
const Table = ({noData}) => {
const { isUninitialized, isLoading, isFetching, data } = useGetUsersQuery()
if (isLoading || isFetching) return <div>Loading...</div>
useEffect(()=> {
if (isUninitialized && !data.length) setNoData(true)
},[data])
if (data) return <table>...</table>
return null
}
However, I am not completely satisfied with any solution. Has someone a better idea?

Related

React: props are undefined, even though they should be

my props in the children class is supposed to be an array of Event objects.
I am checking beforehand, if the array is empty in App.js like this:
function App() {
class Event {
constructor(id, title, date){
this.id = id;
this.title = title;
this.date = date;
}
}
const [events, setEvents] = useState([])
const [ids, setIds] = useState([])
const [safedIds, setSafedIds] = ([])
const [eventsPrep, setEventsPrep] = useState([Event])
useEffect(() => {
fetch('https://someAPI.com')
.then(response => response.json())
.then(
res => {setEvents(res);
console.log(res);
})
.catch(err => console.log(err))
.then(handleIncomingData())
//.then(console.log("was here"))
}, [])
function handleIncomingData () {
if(events.length > 0) {
events.forEach(event => {
ids.push(event["_id"]);
let date = new Date(event["date"]);
eventsPrep.push(new Event(event["_id"], event["title"], date.toDateString()))
})
}
}
return (
<>
<Navbar/>
{eventsPrep.length > 0 ? <Home events={eventsPrep}/> : <></>}
</>
);
}
export default App;
but whenever I try to reach the props in the child component it is considered undefined.
My child component:
import React from 'react'
import SingleEvent from '../../event/SingleEvent'
export const Home = (props) => {
console.log(props.events)
return (
<>
{props?.events
? props.events.forEach((event) => {
console.log('was here 2');
return <SingleEvent title={event.title} start={event.date} />;
})
: 'no upcomming events'}
</>
);
}
Even if I only pass a string down, it is still undefined.
Thanks for help!
In your useEffect() you update events via setEvents(res) and call handleIncomingData() after that.
In handleIncomingData() you use events, but it will still hold the value from previous render / from the initialization as setEvents(res) will not change events immidiately. You can console.log(events) inside that function to investigate this.
Instead you can pass res into handleIncomingData() and use it instead of events inside that function. So in you useEffect you would have:
.then(response => response.json())
.then(
res => {
setEvents(res);
handleIncomingData(res);
})
In addition in handleIncomingData() use setEventsPrep(...) instead of eventsPrep.push(), as mentioned in the comment.
You are returning 2 return statments in the component.
Only need to return 1 return statement in component
export const Home = (props) => {
return (
<>
{props?.events && Object.values(props.events).length > 0
? props.events.forEach((event) => {
console.log('was here 2');
return <SingleEvent title={event.title} start={event.date} />;
})
: 'no upcomming events'}
</>
);
};
Also to check whether eventsPrep has a length > 0 then try it this way if eventsPrep is an object
eventsPrep && Object.values(eventsPrep).length > 0

React component props not getting rendered inside Next.js Page

I'm trying to render data from props in React functional component that look like this:
interface TagsComponentProps {
tags: Tag[];
}
const TagsComponent: FC<TagsComponentProps> = (props: TagsComponentProps) => (
<>
{props.tags.length === 0 &&
<LoadingStateComponent />
}
{props.tags.map(tag => {
{ tag.tagId }
{ tag.tagName }
})
}
</>
)
export default TagsComponent;
Within Next.js page that receiving data inside the getStaticProps method. It looks like that:
const IndexPage = ({ tags }: InferGetStaticPropsType<typeof getStaticProps>) => (
<>
<LayoutComponent>
<TagsComponent tags={tags} />
</LayoutComponent>
</>
)
export default IndexPage;
export const getStaticProps = async () => {
const res = await fetch(`${process.env.HOST}/api/tags/read`)
const data = await res.json()
// if (error) {
// return <ErrorComponent errorMessage={'Ошибка загрузки тегов'} />
// }
return {
props: {
tags: data.Items as Tag[]
}
}
}
But nothing is getting rendered at all although I'm receiving data. Probably I'm missing some concept of data fetching for SSR in Next.js.
I guess the issue is .map() is not returning anything in your code here:
{props.tags.map(tag => {
{ tag.tagId }
{ tag.tagName }
})
}
Instead you should try as the following:
{
props.tags.map(tag => (
<>
{ tag.tagId }
{ tag.tagName }
</>
))
}
Also you can do a null check before as props.tags && props.tags.map().

Cannot update a component from inside the function body of a different component warning

I wrote a component called component1 as below and it is inside the parent component. The component1 is at the bottom of the page and I don't want to render it unless the user scroll down to that area. Thus I use the InView from 'react-intersection-observer' to determine if this area is in view. If so then fetch data and render data. But I get the warning: Warning: Cannot update a component from inside the function body of a different component. What is the reason of getting this warning? Is it because I set the setInView in the component?
<parent>
<component4 />
<component3 />
<component2 />
<component1 />
</parent>
const component1 = () => {
const [inView, setInView] = React.useState(false);
const [loading, error, data] = fetchData(inView); // hook to fetch data
// data is an array
const content = data.map(d => <div>{d}</div>);
const showEmpty = true;
if (data) {
showEmpty = false;
}
return (<InView>
{({ inView, ref }) => {
setInView(inView);
return (
<div ref={ref}>
<div>{!showEmpty && content}</div>
</div>
)
</InView>);
}
As I understood you want to fetch data when the component gets in view right?
here is my solution with useEffect:
const Componet1 = () => {
const [inView, setInView] = useState(false)
const [myData, setMyData] = useState(null)
useEffect(() => {
if (inView && !myData) {
const [loading, error, data] = fetchData(inView); // hook to fetch data
if (data) {
setMyData(data)
}
}
return () => {
// cleanup
}
}, [inView,myData])
return (
<InView as="div" onChange={(inView) => setInView(inView)}>
{myData ? myData.map((d, i) => <div key={i}>{d}</div>) : 'Loading...'}
</InView>
);
}
Also, this fetches data just once.

React update sibling component

I'm confused about react hot updating components.
I've got something like this:
const SingleEvent = ({ event }) => (
<>{event.status}</>
)
const EventDetails = ({ event, updateEvent }) => (
<button
onClick={async () => {
const data = await getAPIResponse(); // { status: 'open' }
updateEvent(event.id, data)
}
>
Update
</button>
)
const List = ({ events, updateEvent, selectedEvent }) => {
if (selectedEvent) {
return <EventDetails event={selectedEvent} updateEvent={updateEvent} />
}
return (
<>
{events.map(event => <SingleEvent event={event}/>)}
</>
)
}
const Page = ({ initialEvents }) => {
const [events, setEvents] = useState(initialEvents || []);
const [selectedEvent, setSelectedEvent] = useState(null);
const updateEvent = (eventId, data) => {
setEvents(prevState => {
const eventIndex = prevState.findIndex(
element => element._id === eventId,
);
if (eventIndex === -1) {
return prevState;
}
prevState[eventIndex] = {
...prevState[eventIndex],
...data,
};
return prevState;
});
};
return <List events={events} updateEvent={updateEvent} selectedEvent={selectedEvent} />
}
In the <EventDetails /> component I'm updating one of the events (basically changing it's status). If API works fine, when I close the details (set the selectedEvent to null) everything is changing as it should. If I close the the details before getting the API response - nothing changed.
I've checked the updateEvent function, and it's performing the update, but the UI is not refreshed.
To be clear:
I Open the <EventDetails /> component, I'm pressing the button to update the event. The API should change its status. When I close the EventDetails I'm getting a List of <SingleEvent /> components. Every one of them displays the event.status.
If I close the EventDetails before getting response, status in SingleEvent is not updating. If I wait for the response everything works ok.
Since the component is unmounted before the data is fetched, its no longer able to update the response.
You can instead provide a function as props which performs the API requst and updates the status
const EventDetails = ({ handleClick }) => (
<button
onClick={handleClick}
>
Update
</button>
)
const List = ({ events, updateEvent, selectedEvent }) => {
const handleClick = async () => {
const data = await getAPIResponse(); // { status: 'open' }
updateEvent(event.id, data)
}
if (selectedEvent) {
return <EventDetails event={selectedEvent} updateEvent={updateEvent} handleClick={handleClick}/>
}
return (
<>
{events.map(event => <SingleEvent event={event}/>)}
</>
)
}
The problem was with the update method. Operating directly on prevState is not a good idea. After I changed the updateEvent function, everything works fine.
setEvents(prevState =>
prevState.map(event => {
if (event._id === eventId) {
return {
...event,
...updatedEvent,
};
}
return event;
})
);

Losing router object on child component rerender

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.

Categories