I want to show loading indicator outside of my query component.
Right now I'm using redux and when I start loading data from api, I set redux state attribute which I then use to show loader component.
How to achieve same thing with Apollo Client Query/Mutations?
Every example shows loading prop inside Query but how to show loader outside of this Query?
Lets say I have something like this:
<Header><Loader/></Header>
<Content><Query><List /></Query></Content>
But I do not want to wrap everything with Query and do something like this:
<Query>
<Header><Loader/></Header>
<Content><List ></Content>
</Query>
Is it even possible ?
Should i mutate #client cache inside Query when loading ?
Edit: I want to replace redux with apollo-link-state, so solution should not use redux.
I would suggest to use react-apollo-hooks, it make Apollo usage much more easy and fun.
Have a look at sample code
import React from 'react';
import { useQuery } from 'react-apollo-hooks';
import get from 'lodash.get';
import SUBSCRIPTIONS_QUERY from './Subscriptions.graphql';
import s from './SubscriptionTable.scss';
const SubscriptionsTable = () => {
const { data, loading, error } = useQuery(SUBSCRIPTIONS_QUERY);
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
return (
<div className={s.SubscriptionTable}>
<div className={s.SubscriptionTable__Header}>Email</div>
{get(data, 'subscriptions', []).map(subscription => (
<div key={get(subscription, 'id')} className={s.SubscriptionTable__Row}>
{get(subscription, 'email')}
</div>
))}
</div>
);
};
Firstly, there's no reason to put your loading state in Redux, but if you really want to for some reason you should set your redux state to loading: true when the component mounts, and set your redux state to loading: false when loading is no longer true in the component. Here's what I mean:
import React from 'react';
import {
useQuery,
useEffect
} from 'react-apollo-hooks';
import get from 'lodash.get';
import SUBSCRIPTIONS_QUERY from './Subscriptions.graphql';
import s from './SubscriptionTable.scss';
const SubscriptionsTable = () => {
const {
data,
loading,
error
} = useQuery(SUBSCRIPTIONS_QUERY);
useEffect(() => {
// set loading: true in redux state
// as soon as component mounts
// via action/reducer pattern
}, [])
if (loading) {
return "Loading..."
} else {
// set loading: false in redux state
// when query no longer loading
// via action/reducer pattern
}
if (error) return `Error! ${error.message}`;
return (<div>{JSON.stringify(data)}</div>);
};
Related
I'm trying to take the function MainMenu and getStaticProps from being in the same page (index.js) and break it up into components. Here is the index.js page below that is working good.
#index.js
import Link from 'next/link';
function MainMenu({ menuLists }) {
return (
<div>
{menuLists.map(menuItem => (
<div>
<Link href={menuItem.absolute}><a>{menuItem.title}</a></Link>
{menuItem.below && menuItem.below.map(childItem => (
<div>
<Link href={childItem.absolute}><a>{childItem.title}</a></Link>
</div>
))}
</div>
))}
</div>
)
}
export async function getStaticProps() {
const response = await fetch('http://localhost:8888/api/menu_items/main');
const menuLists = await response.json();
return {
props: {
menuLists: menuLists,
},
}
}
export default MainMenu
I have created fetch-mainmenu.js in a lib directory with the following code.
#fetch-mainmenu.js
export async function loadMainMenu() {
const response = await fetch('http://localhost:8888/api/menu_items/main')
const menuLists = await response.json()
return {
props: {
menuLists: menuLists,
},
}
}
I then created sidebar.js to show the menu system from the json file. The sidebar.js file is working because the hard coded menus are showing.
# sidebar.js
import Link from 'next/link'
import styles from './sidebar.module.css'
import { loadMainMenu } from '../lib/fetch-mainmenu'
export default function Sidebar( { menuLists } ) {
const menus = loadMainMenu()
return (
<nav className={styles.nav}>
<input className={styles.input} placeholder="Search..." />
<Link href="/">
<a>Home</a>
</Link>
<Link href="/about">
<a>About</a>
</Link>
<Link href="/contact">
<a>Contact</a>
</Link>
</nav>
)
}
Getting the following error "TypeError: Failed to fetch".
What is the best way of getting this done using components.
Solution
1. Prop Drilling
Easy. Just send down all the data from getStaticProps(). This is the safest bet at current stage but it may create some redundant props.
// I've omitted fetch().json() to ease the reading. just assume it's a proper code.
const MainMenuComponent = ({menuLists}) => {
return <div>{menuLists}</div>
}
const MainPage = ({menuLists}) => {
return <MainMenuComponent menuLists={menuLists} />
}
export async function getStaticProps() {
const req = await fetch('...');
return {
props: {
menuLists: req,
},
}
}
export default MainPage
2. React.useEffect
A React component can't have asynchronous code inside render code. It is pretty obvious in a class component but it's hard to tell in a functional component
// I've omitted fetch().json() to ease the reading. just assume it's a proper code.
// class component
class SampleComponent extends React.Component {
constructor(props) {
super(props);
this.state = { data: {} };
}
async getData() {
// ✅ this works
const data = await fetch('...');
// data has to be put in state because it's asynchronous.
this.setState({ ...this.state, data });
}
componentDidMount() {
this.getData();
}
render() {
// ❌ this can't happen here because render is synchronous
await fetch('...');
// use state to display asynchronous data.
return <h1>Hello, {JSON.stringify(this.state.data)}</h1>;
}
}
// functional component
function SampleComponent = () => {
// everything outside `useEffect, useLayoutEffect` is mostly assumed as render function.
// ❌ thus, this does not work here
await fetch('...');
const [data, setData] = useState({});
useEffect(async () => {
// everything inside here treated as componentDidMount()
// not the exact same thing though.
// ✅ this works!
setData(await fetch('...'))
}, []);
return <h1>Hello, {JSON.stringify(data)}</h1>
}
WARNING if there's getStaticProps inside your page, it means the component also has to be synchronous. If the rendered component changes its content in a very short time, in a fraction of second, then it may get rehydration error. It needs to be wrapped with dynamic() so that the Next.js can ignore the component when rendering server-side & rehydrating the component. Please refer to Next.js official document on Dynamic Import.
It does work but the code seems long.
3. TanStack Query(or React-Query) or useSWR
There are nice 3rd party libraries that help writing asynchronous data fetching code inside a react component; TanStack Query and SWR are the most well known. These libraries also implement caching and revalidation. It can help handling complex issues invoked due to asynchronous requests.
// example code from useSWR
import useSWR from 'swr'
function Profile() {
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
4. State Management with Context
Most cases are easily dealt with the Query-SWR solution but if the app gets big enough, there could be a need to synchronize the data.
In that case, fetch the data in server code and share the data with a central state management library(a.k.a store libs). A good example is this github repo of Zustand + Next.js. A bare React.Context can be used as well.
However, this method can get very complicated later, maybe not suitable for an inexperienced team; it's basically similar to building another complex layer as big as backend. That's why the trend has moved to Query-SWR solution these days. Still, this comes handy in certain cases.
import { useStore } from "../lib/store";
const SampleComponent = () => {
const { data } = useStore();
return <div>{JSON.stringify(data)}</div>
}
const MainPage() {
return <SampleComponent />
}
export async function getStaticProps() {
// refer to the github repo for this store structure
const zustandStore = initializeStore();
// this is a POC. the actual code could be different.
// store data is updated, and can be used globally in other components in a synchronized state.
const data = await useStore.setData(await fetch('...'));
return {
props: {
initialZustandState: JSON.parse(JSON.stringify({ ...zustandStore.getState(), data })),
},
};
}
5. Server-side Component
With the emergence of React 18 server side component, Next.js is also working on Next.js Server Components.
This implementation is probably the closest implementation to code from the question. Nevertheless, the work is still in progress and highly unstable.
I've kept my eyes on this method for about a year but the implementation has been constantly changing. Until we get the stable release, this can wait.
I was asked a question regarding SWRs "loading" state:
How do you create a loading state from SWR between different URL fetches?
Their docs make it appear straight forward:
const { data, error } = useSWR(`/api/user/${id}`, fetcher)
const isLoading = !error && !data;
However, this logic seems to fail after the first render of the hook/component. On the first render data is undefined. Then loads and data becomes a value to consume in the UI.
Let's say I change the id via the UI and want to show loading indicator. Because data is no longer undefined, the same logic fails.
There is an additional item returned isValidating. So I updated my logic:
const isLoading = (!data && !error) || isValidating
However, this could be true when:
there's a request or revalidation loading.
So in theory something else causes my component to rerender. This could inadvertently cause a "revalidation" and trigger loading state gets shown. This could break the UI temporarily, by accident.
So how do you derive "loading" between URL changes without revalidation? I am trying to replicate how graphQL Apollo Client returns const { loading, error, data } = useQuery(GET_DOGS);
Let's say I change the id via the UI and want to show loading indicator. Because data is no longer undefined, the same logic fails.
data will be undefined again when you change the key (id), if it doesn't have a cache value.
Remember that in SWR { data } = useSWR(key) is mentally equivalent to v = getCache(k), where fetcher (validation) just write to the cache and trigger a re-render.
data is default to undefined, and isValidating means if there's an ongoing request.
Alternatively, you can derive loading through the use of middleware. Here's what I use...
loadingMiddleware.ts
import { useState } from 'react'
import { Middleware } from 'swr'
const loadingMiddleware: Middleware = (useSWRNext) => (key, fetcher, config) => {
const [loading, setLoading] = useState(false)
const extendedFetcher = (...args) => {
setLoading(true)
try {
return fetcher(...args)
} finally {
setLoading(false)
}
}
const swr = useSWRNext(key, extendedFetcher, config)
return { ...swr, loading }
}
export default loadingMiddleware
App.tsx
import { SWRConfig } from 'swr'
import loadingMiddleware from './loadingMiddleware'
const App: FC = () => {
...
return (
<SWRConfig value={{ use: [loadingMiddleware] }}>
...
</SWRConfig>
)
}
export default App
Update (12/13/22)
swr#v2 is out and provides isLoading and isValidating properties in the return value of useSWR.
Here's the difference between the two according to the swr docs.
isValidating becomes true whenever there is an ongoing request whether the data is loaded or not.
isLoading becomes true when there is an ongoing request and data is not loaded yet.
I know that question was asked a number of times, but none of answers work for me. I got the component, which job is to display user data from react-redux BUT userData can be stored in localStorage and on first load of website this localStorage is fetched to redux state, so I get the error where I want to display initials of currently logged in user. After adding async/await to mapStateToProps function localStorage gets fetched, but I still got error saying 'Cannot get 0 of undefined', using useEffect with pre set initials also gives an error. Here is my current code:
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { ReactComponent as Person } from 'assets/person.svg'
import { ReactComponent as Add } from 'assets/add.svg'
import { ReactComponent as Settings } from 'assets/settings.svg'
import { Styled, Container, Button } from './styles'
const UserInfo = ({ initial }) => {
let initials = 'UN'
useEffect(() => {
initials = initial
}, [initial])
return (
<Styled>
{initials}
<Container>
<Button>
<Add />
</Button>
<Button>
<Person />
</Button>
<Button>
<Settings />
</Button>
</Container>
</Styled>
)
}
const mapStateToProps = async ({ user }) => {
const initial = await `${user.name[0]}${user.surname[0]}`
return initial
}
export default connect(mapStateToProps)(UserInfo)
UserInfo.propTypes = {
initial: PropTypes.string.isRequired,
}
I also tried returning initial: initial || 'UNDEF' but I still receive an error. Additionally, when I don't refresh the browser on VSC save, I don't get an error. How can I force my component to wait for props from react-redux
I did it a little around, but now it works, i used useState and useEffect hooks and changed a bit mapStateToProps, that's my solution:
const [initials, setInitials] = useState('')
useEffect(() => {
if(name && surname) {
setInitials(`${name[0]}${surname[0]}`)
}
}, [name, surname])
and simple mapStateToProps
const mapStateToProps = ({ user }) => ({
name: user.name,
surname: user.surname,
})
This is not something that can be awaited: await '${user.name[0]}${user.surname[0]}' which is why you're still getting an error. You shouldn't need (or want, see https://react-redux.js.org/using-react-redux/connect-mapstate#mapstatetoprops-functions-should-be-pure-and-synchronous) to make mapStateToProps async. You're likely already doing the async call with an async redux action. You should either check for the presence of user name/surname and return a default state if undefined
const initial = user.name && user.surname ? '${user.name[0]}${user.surname[0]}' : 'UNDEF';
or add an isUserLoaded flag for the user state and return the default state, loading indicator, or not render that portion of UI until that flag is true.
I'm new to Gatsby & React and I'm trying to figure out how to get the best of both worlds of prerendering and dynamic data.
The query alone works great for getting the data at build time and passing it as a prop to the Menu component where each menu item is rendered. However, at run time, I would like to pull the data again from the DB and have it update the data, for example, if there was a price change, etc.
I know I could just rebuild the whole project but I would like to have this as a fallback.
How can I make the query send the data to the Menu component and then also [send the data again?] when the DB call is done.
Code that is currently not working as expected:
index.jsx
import React, { useEffect } from "react"
import Layout from "../components/layout"
import SEO from "../components/seo"
import Menu from '../components/menu'
import { graphql } from "gatsby"
import firebase from "gatsby-plugin-firebase"
const IndexPage = (props) => {
useEffect(() => {
// use this hook to make db call and re-render menu component with most up to date data
var db = firebase.firestore();
let docs = []
db.collection(`public/menu/${process.env.restaurantId}`).get().then(val => {
val.forEach(doc => {
docs.push({ node: doc.data() })
});
console.log('docs', docs)
props.data.allMenuItem.edges = docs; // i have no idea what i'm doing
})
}, [])
return (
<Layout>
<SEO title="Home" />
<Menu menuItems={props.data.allMenuItem.edges}></Menu>
</Layout>
)
}
// use this query for prerendering menu items
export const query = graphql`
query MyQuery {
allMenuItem {
edges {
node {
available
name
group
}
}
}
}
`;
export default IndexPage
You aren't supposed to modify React properties; any value that can change should be part of the state of the component. See Can I update a component's props in React.js?
However, the following code ought to do it. Create a state and give it the property value as default value. Then update it after the data loads on the client side.
const IndexPage = props => {
const [menuItems, setMenuItems] = useState(props.data.allMenuItem.edges.map(({node}) => node))
useEffect(() => {
// use this hook to make db call and re-render menu component with most up to date data
let db = firebase.firestore()
db.collection(`public/menu/${process.env.restaurantId}`)
.get()
.then(setMenuItems)
}, [])
return (
<Layout>
<SEO title="Home" />
<Menu menuItems={menuItems}></Menu>
</Layout>
)
}
Note that I've switched to using the data format you get from firestore (without node) rather than the one from Gatsby, so you'd need to modify your Menu component to not expect an extra level of nesting (with node) if you use this code.
I have one fetch and one display .js file. However I am trying to figure out how to read the state. Of course as it's done now it's returned from the other .js file. But I would like to use the state that was set instead. How would I refactor to do this?
I would like to use the stateURL prop in the DataLoader.js
DataLoader.js
import React, { useState, useEffect } from 'react';
import useFetch from "./useFetch";
export default function DataLoader({stateURL}) {
const data = useFetch("/api");
// Should not be used
console.log("data", data);
const data2 = Object.keys(data).map(data => data);
console.log("data2", data2);
const data3 = data2.map(key => {
console.log("inside data3", key );
return data[key];
});
//This is empty
console.log("state", stateURL);
return (
<div>
<h1>Testing</h1>
<ul>
{Object.keys(data3).map(key => {
return <li>{data3[key].href}</li>;
})}
</ul>
</div>
);
}
useFetch.js
import { useState, useEffect } from "react";
export default function useFetch(url) {
const [stateURL, setStateURL] = useState([]);
console.log("url", url);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => setStateURL(data._links));
}, []);
console.log("stateURL", stateURL);
return stateURL;
}
That is not possible. The hooks can only be referred from the original creating component.
Why do you just use the fetch hook within the display file?
If you want to keep these two components separated:
To access the data, you have to share the data somehow to be accessible to your other components. There are several ways to do it:
Pass the data up into the parent component via a callback and pass that into the other child component.
Using a state management library like Redux or Mobx.
Using the context API to share data between components but that might not be the best way for this kind of data.
It depends on your setup and your needs. If only these two components ever need that data, pushing it into the parent works fine.
If there are other components, which maybe need that data, you might want to use a state management lib.