React Apollo Query based on a condition - reactjs

I'm currently building an app (hybrid mobile app) to show a list of records (places). Following is my requirement,
1) If the app is online, get the details from the server.
2) If the app is offline, get the details from the local storage.
I can get each condition working by its own. However (I'm fairly new to react and apollo-react), I'm not sure how to add a condition to the query.
below is an example of my query on getting the data from the server (I have this part working)
const client = new ApolloCient({
uri: "/graphql"
});
const PLACES_LIST = gql`
{
places {
id
title
}
}
`;
class PlacesList extends React.Component {
render() {
return (
<ApolloProvider client={client}>
<Query query={PLACES_LIST}>
{({ loading, data }) => {
if (loading) return "Loading....";
const { places } = data;
return places.map(place => (
<PlaceDetail
key={place.id}
place={place}
></PlaceDetail>
));
}}
</Query>
</ApolloProvider>
);
}
}
pseudocode for this I'm thinking would be,
if (online) {
# run apollo client
} else {
# read from the local storage
}
Can anyone point me in the correct direction. TIA.
Also, I'm using a latest version of react and I have the flexibility of using react hooks if that required.

const client = new ApolloCient({
uri: "/graphql"
});
const PLACES_LIST = gql`
{
places {
id
title
}
}
`;
class PlacesList extends React.Component {
render() {
return (
<ApolloProvider client={client}>
<Query query={PLACES_LIST}>
{({ loading, data, error }) => {
// There is also an error parameter from the hook
if (loading) return "Loading....";
// Here You can decide if its a connection error or smt other.
// I would recoment the fetchPolicy="network-only" prop for your <Query> in this case
if(error) {
return localstorage.getItem("Smt");
} else {
const { places } = data;
return places.map(place => (
<PlaceDetail
key={place.id}
place={place}
></PlaceDetail>
));
}
}}
</Query>
</ApolloProvider>
);
}
}

Maybe you can try to check for network connection using the navigator interface. I am not sure if navigator.onLine is available in hybrid mobile apps, but it would be easy to check.
You could do something like:
render(){
const isOnline = navigator.onLine
return (
<div>
{isOnline ? (
<ApolloProvider client={client}></ApolloProvider>
) : (
<LocalStorageComponent />
)}
</div>
);
}

Related

React useContext, NextJS static page generation, and rendering

I'm using React useContext to avoid prop-drilling, and building static pages in NextJS, as described in this Technouz post (NB: this is not about the NextJS getStaticProps context parameter).
The basic functionality is working; however, I can't figure out the right way to update the context from components farther down the chain.
At a high level, I have this:
// pages/_app.js
function MyApp({ Component, pageProps }) {
const [ headerData, setHeaderData ] = useState( {
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data?.siteBranding.siteName,
companyLogo: pageProps.data?.siteBranding.companyLogo,
menu: pageProps.data?.menu
} );
return (
<HeaderProvider value={{ headerData, setHeaderData }}>
<Header />
<Component {...pageProps} />
</HeaderProvider>
)
}
// components/Header.js
export default function Header() {
const { headerData } = useHeader();
return (
<header>
{ headerData.urgentBanner && <UrgentBanner {...headerData.urgentBanner}/> }
<Navbar />
</header>
)
}
// lib/context/header.js
const HeaderContext = createContext();
export function HeaderProvider({value, children}) {
return (
<HeaderContext.Provider value={value}>
{children}
</HeaderContext.Provider>
)
}
export function useHeader() {
return useContext(HeaderContext);
}
The Navbar component also uses the context.
That all works. I query the data from a headless CMS using getStaticProps, and everything gets passed through pageProps, and when I run npm run build, I get all of my static pages with the appropriate headers.
But, now I'm extending things, and not all pages are the same. I use different models at the CMS level, and want to display different headers for landing pages.
Inside of [pages].js, I handle that thusly:
const Page = ({ data }) => {
switch (data.pageType) {
case 'landing-page':
return (
<PageLandingPage data={data} />
);
case 'page':
default:
return (
<PageStandard data={data} />
);
}
}
Now, if we're building a static landing page instead of a static standard page, the whole hierarchy would look something like this:
<HeaderProvider value={{ headerData, setHeaderData }}>
<Header>
{ headerData.urgentBanner && <UrgentBanner {...headerData.urgentBanner}/> }
<Navbar>
<ul>
{menu && <MenuList type='primary' menuItems={menu.menuItems} />}
</ul>
</Navbar>
</Header>
<PageLandingPage {...pageProps}> // *** Location 2
<LandingPageSection>
<Atf> // *** Location 1
<section>
{ socialProof && <SocialProof { ...socialProof } />}
<Attention { ...attentionDetails }/>
</section>
</Atf>
</LandingPageSection>
</PageLandingPage>
</HeaderProvider>
Location 1 and Location 2 are where I want to update the context. I thought I had that working, by doing the following at Location 1:
// components/Atf.js
export default function Atf({content}) {
// this appeared to work
const { headerData, setHeaderData } = useHeader();
setHeaderData(
{
...headerData,
urgentBanner: content.find((record) => 'UrgentBannerRecord' === record?.__typename)
}
)
return (
<section>
{ socialProof && <SocialProof { ...socialProof } />}
<Attention { ...attentionDetails }/>
</section>
)
}
I say "thought", because I was, in fact, getting my <UrgentBanner> component properly rendered on the landing pages. However, when digging into the fact that I can't get it to work at Location 2, I discovered that I was actually getting warnings in the console about "cannot update a component while rendering a different component" (I'll come back to this).
Now to Location 2. I tried to do the same thing here:
// components/PageLandingPage.js
const PageLandingPage = ({ data }) => {
const giveawayLandingPage = data.giveawayLandingPage;
// this, to me, seems the same as above, but isn't working at all
if (giveawayLandingPage?.headerMenu) {
const { headerData, setHeaderData } = useHeader();
setHeaderData(
{
...headerData,
menu: { ...giveawayLandingPage.headerMenu }
}
);
}
return (
<div>
{giveawayLandingPage.lpSection.map(section => <LandingPageSection details={section} key={section.id} />)}
</div>
)
}
To me, that appears that I'm doing the same thing that "worked" in the <Atf> component, but ... it's not working.
While trying to figure this out, I came across the aforementioned error in the console. Specifically, "Cannot update a component (MyApp) while rendering a different component (Atf)." And I guess this is getting to the heart of the problem — something about how/when/in which order NextJS does its rendering when it comes to generating its static pages.
Based on this answer, I initially tried wrapping the call in _app.js in a useEffect block:
// pages/_app.js
...
/* const [ headerData, setHeaderData ] = useState( {
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data?.siteBranding.siteName,
companyLogo: pageProps.data?.siteBranding.companyLogo,
menu: pageProps.data?.menu
} ); */
const [ headerData, setHeaderData ] = useState({});
useEffect(() => {
setHeaderData({
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data?.siteBranding.siteName,
companyLogo: pageProps.data?.siteBranding.companyLogo,
menu: pageProps.data?.menu
});
}, []);
But that didn't have any impact. So, based on this other answer, which is more about NextJS, though it's specific to SSR, not initial static page creation, I also wrapped the setState call in the <Atf> component at Location 1 in a useEffect:
// components/Atf.js
...
const { headerData, setHeaderData } = useHeader();
/* setHeaderData(
{
...headerData,
urgentBanner: content.find((record) => 'UrgentBannerRecord' === record?.__typename)
}
) */
useEffect(() => {
setHeaderData(
{
...headerData,
urgentBanner: content.find((record) => 'UrgentBannerRecord' === record?.__typename)
}
)
}, [setHeaderData])
That did stop the warning from appearing in the console ... but it also stopped the functionality from working — it no longer renders my <UrgentBanner> component on the landing page pages.
I have a moderately good understanding of component rendering in React, but really don't know what NextJS is doing under the covers when it's creating its initial static pages. Clearly I'm doing something wrong, so, how do I get my context state to update for these different types of static pages?
(I presume that once I know the Right Way to do this, my Location 2 problem will be solved as well).
I ended up fixing this by moving from useState to useReducer, and then setting all of the state, including the initial state, at the page level. Now, _app.js is simplified to
function MyApp({ Component, pageProps }) {
return (
<HeaderProvider>
<Header />
<Component {...pageProps} />
</HeaderProvider>
)
}
export default MyApp
And the context hook setup uses the reducer and provides it back to the provider:
// lib/context/header.js
const initialState = {};
const HeaderContext = createContext(initialState);
function HeaderProvider({ children }) {
const [headerState, dispatchHeader] = useReducer((headerState, action) => {
switch (action.type) {
case 'update':
const newState = { ...headerState, ...action.newState };
return newState;
default:
throw new Error('Problem updating header state');
}
}, initialState);
return (
<HeaderContext.Provider value={{ headerState, dispatchHeader }}>
{children}
</HeaderContext.Provider>
);
}
function useHeader() {
return useContext(HeaderContext);
}
export { HeaderProvider, useHeader }
Then, everywhere you want to either get the state or set the state, as long as you're inside of the <Provider>, you're good to go. This was a little confusing at first, because it's not obvious that when you useContext, what it's doing is returning the current value, and the value is provided both with the state, and with the dispatch function, so when you want to set something, you query the "value", but destructure to get the "setter" (i.e., the dispatch function).
So, for example, in my "location 2" from the initial question, it now looks like
import React, { useEffect } from 'react';
import { useHeader } from '../lib/context/header';
const PageLandingPage = ({ data }) => {
const giveawayLandingPage = data.giveawayLandingPage;
// here's where we get the "setter" through destructuring the `value`
// let foo = useHeader();
// console.log(foo);
// > { headerState, dispatchHeader }
const { dispatchHeader } = useHeader();
useEffect(() => {
dispatchHeader({
newState: {
menu: { ...giveawayLandingPage.headerMenu }
},
type: 'update'
});
}, []);
...

React set provider variables on application load

In the root of my app I need to make calls to an api when the app loads and use that data in the provider.
const App = () => {
const [userRoles] = useState(['SuperUser']);
const [selectedProfileId, setSelectedProfileId] = useState(null);
const { isLoading, user, error } = useAuth0();
const { data, loading: profileLoading, error: profileError } = useQuery(
GET_PROFILES
);
useEffect(() => {
setProfileId(data?.items?.[0]?.id);
}, [data]);
if (isLoading || profileLoading) return <Loading />;
if (error)
return (
<Alert />
);
if (searchCustomersError) {
return (
<Alert />
);
}
// const getUserRole = () => {return role} ??? How can I call this in the roles prop in the provider?
return (
<ProfileProvider
id={1}
name={user?.name}
roles={userRoles}
setSelectedProfile={handleProfielChange}
selectedProfileId={selectedProfileId}
>
... App routes
</ProfileProvider>
);
};
The problem is that selectedProfileId is null initially and my app uses that null value in the provider for some reason then once it gets the data it gives the provider the proper value. I dont get it because if the profileLoading value is true then the Provider shouldnt render right?
Also, I need to have a function that sets the roles variable right now I have it static but I need to adjust the the user data I get from user Auth0 to get the userRole var. How can I do this?
Have you tried doing something like so, it'll check to see if the profileLoading is false, then render ProfileProvider, otherwise you create some sort of loading screen or something before the render.
{!profileLoading?
<>
<ProfileProvider
id={1}
name={user?.name}
roles={userRoles}
setSelectedProfile={handleProfielChange}
selectedProfileId={selectedProfileId}
>
... App routes
</ProfileProvider>
</> :
<Loading />
}

TypeError: Cannot read property 'getPosts' of undefined - useQuery hook, react Functional Components

I did try searching for the same question but all of those were of either angular or unrelated,
I am trying to make a Social app using MongoDB, Express, React, Node, Graphql with Apollo, I am following a video from freecodecamp : Link to the video
In that video everything worked fine but in his deployed version he is having the same error as mine
react_devtools_backend.js:2450 TypeError:
Cannot read property 'getPosts' of undefined
at ae (Home.js:14)
at Jo (react-dom.production.min.js:3274)
link to the deployed app
My Code: I am dropping a link to my github repo containing the whole project : Link to github
repo
Stack Overflow was throwing too many indentation issues so i have linked my github above as there
is too much of code
I'm using semantic-ui for styling
I'm using graphql the fetch posts from MongoDB
Apollo Client for rendering data
This is the error I am getting in the Home.js:
Screen Shot of the error:
Make it simpler to debug, instead:
const {
loading,
data: { getPosts: posts }
} = useQuery(FETCH_POSTS_QUERY);
do:
const { data, loading, error } = useQuery(FETCH_POSTS_QUERY);
if(data) {
console.log(data);
const { getPosts: posts } = data;
}
if(error) {
console.log(error);
return "error"; // blocks rendering
}
this works but not when data is there and not always
"not when data", "not always"??? weird ... 'posts' can be defined only if data exists ... accessing it when undefined will fail, always ... you must check 'data'
You can/should render items (posts) ONLY when:
!loading
AND
data != undefined - if(data) or (data && in JSX
{loading && <h1>Loading posts..</h1>}
{data && (
<Transition.Group>
{posts &&
posts.map((post) => (
<Grid.Column key={post.id} style={{ marginBottom: 20 }}>
<PostCard post={post} />
</Grid.Column>
))}
</Transition.Group>
)}
use this code like this
const { loading, data: { posts } = {} } = useQuery(FETCH_POSTS_QUERY);
You need to define the query operation like:
export const FETCH_POSTS_QUERY = gql`
query GetPosts {
getPosts {
// fields
}
}
`
Alternatively, you can make use of alias to easily reference them.
export const FETCH_POSTS_QUERY = gql`
query GetPosts {
posts: getPosts {
// fields
}
}
`
const {
loading,
data: { posts } // uses alias directly. no need to rename
} = useQuery(FETCH_POSTS_QUERY);
const { loading, data: { getPosts: posts } = {} } = useQuery(FETCH_POSTS_QUERY)
This should solve the problem
THIS WILL WORK
write data.getPosts inside the grid
const { loading ,data , error } = useQuery(FETCH_POSTS_QUERY);
if (error) return Error! ${error.message};
{loading ? (<h1>Loading posts...</h1>)
: (data.getPosts &&
data.getPosts.map((post) => (
<Grid.Column key={post.id} style= {{ marginBottom: 20}}>
<PostCard post={post} />
</Grid.Column>

React GraphQL - How to return the results of a Query component as an object to use in React context

I have a Query component which gets the information of the a user who has logged in. The query itself works but I am having trouble returning the results of the query as an object which I then want to pass to React.createContext()
I am using Apollo client to make my Query component in my React application.
The following is an example of my current code:
function getUser() {
return (
<Query query={query.USER_INFO}>
{({ loading, error, data }) => {
if (loading) return <div>Loading</div>;
if (error) return <div>error</div>;
const userInfo = {
name: data.user.name,
age: data.user.age,
}
}}
</Query>
);
}
//need userInfo object to go here
export const UserContext = React.createContext(userInfo);
How can I get the return of the Query to then use in React.createContext? The reason I want to do it this way is to avoid rewriting this Query in every component where I want info of the user who has logged in.
To be honest, to me it seems like you're just missing a return statement:
function getUser() {
return (
{({ loading, error, data }) => {
if (loading) return Loading;
if (error) return error;
const userInfo = {
name: data.user.name,
age: data.user.age,
}
return userInfo // ----- this here
}}
</Query>
);
}
//need userInfo object to go here
export const UserContext = React.createContext(userInfo);
but i didn't test it out and haven't taken this approach - give it a go see, if it helps

Trouble debouncing setState in Context (react-native)

I'm rendering a lot of Text components and saving positions (in order to scroll to them), but I would like to improve performance by "bundling" it by a debounce (and are open for other suggestions that would improve performance).
...
import debounce from 'lodash/debounce'
...
class ContextProvider extends Component {
....
setContentPosition = (layout, key) => {
const contentPos = { ...this.state.contentPos }
contentPos[key] = layout.nativeEvent.layout.y
// TODO: make it debounce https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js
this.setState({ contentPos }, () => console.log('contPos updated'))
}
...
render() {
return (
<Provider
value={{
setContentPosition: this.setContentPosition,
}}
>
{this.props.children}
</Provider>
)
}
}
I have tried a couple of different combinations, without luck. Was expecting this to work:
...
render() {
return (
<Provider
value={{
setContentPosition: debounce(this.setContentPosition, 200),
}}
>
{this.props.children}
</Provider>
)
}
}
It throws the following error:
Update 01
The following change (for contentPos[key])
setContentPosition = (layout, key) => {
const contentPos = { ...this.state.contentPos }
//console.log(layout.nativeEvent)
contentPos[key] = layout.nativeEvent
// TODO: make it debounce https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js
this.setState({ contentPos }, () => {
console.log('contPos updated')
})
}
displays this warning instead:
The position is to be used for scrolling to a text component (when searching) and I have some testing code that scrolls to some Text component - works on iOS but not on Android?

Resources