I'm building out a new marketing website for my company in Gatsby, utilizing axios to fetch data from a REST api to dynamically display a list of dealers, and then dynamically displaying the contact information of the chosen dealer throughout the site. This is done by setting a cookie, which carries the ID of the dealer on the API, and then will fetch that dealer's info based on the cookie. However, I'm encountering an issue where the name of the dealer, which I'm currently displaying in the header, flickers on every page load. It looks bad, so I'm wondering if there is a way to either cache that data, or not force it to fetch on every page load and eliminate the flicker. I'm still in development, so I've got it staged on Netlify here, and you can take a look at the live version.
Here is my hook.
use-fetch.ts
import { useEffect, useState } from 'react';
import axios from 'axios';
export const useFetch = (url: string) => {
const [status, setStatus] = useState('idle');
const [data, setData] = useState([]);
useEffect(() => {
if (!url) return;
const fetchData = async () => {
setStatus('fetching');
const result = await axios(url);
setData(result.data);
setStatus('fetched');
};
fetchData();
}, [url]);
return { status, data };
};
I'm then able to consume this in the pages like so:
const [query] = useState('https://jsonplaceholder.typicode.com/users/1/');
const url = query && 'https://jsonplaceholder.typicode.com/users/${cookie}';
const { data } = useFetch(url);
This sets an initial state users/1/ that will display the information for the first dealer unless a cookie is set.
I use this in a layout component, and I can pass the data prop down to my Header component.
app-layout.tsx
import React, { ReactNode, useEffect, useState } from 'react';
import Logo from '../../assets/svg/logo.svg';
import { Header } from '../header/Header';
import { Footer } from '../footer/Footer';
import { Devtools } from '../devtools/Devtools';
import s from './AppLayout.scss';
import { useCookie } from 'hooks/use-cookie';
import { useFetch } from 'hooks/use-fetch';
interface AppLayoutProps {
menuItems: any;
children: ReactNode;
}
const isDev = process.env.NODE_ENV === 'development';
// tslint:disable no-default-export
export default ({ children, menuItems }: AppLayoutProps) => {
// copyright year
const [year, setDate] = useState<any>();
// setting cookie to be referenced in the useFetch hook, setting the query for dealer specific information
const [cookie] = useCookie('one-day-location', '1');
// the API call
const [query, setQuery] = useState('https://jsonplaceholder.typicode.com/users/1/');
const url = query && `https://jsonplaceholder.typicode.com/users/${cookie}`;
const { data } = useFetch(url);
const getYear = () => setDate(new Date().getFullYear());
useEffect(() => {
getYear();
}, []);
return (
<div className={s.layout}>
<Header menuItems={menuItems} data={data}></Header>
{children}
<Footer menuItems={menuItems} logo={<Logo />} year={year} />
{isDev && <Devtools />}
</div>
);
};
And this is my use-cookie hook that is referenced throughout these components:
use-cookie.ts
import { useState } from 'react';
import Cookies from 'universal-cookie';
/**
* Custom hook creates and returns cookie values.
*/
export const useCookie = (key: string, value: string) => {
const cookies = new Cookies();
const [cookie] = useState(() => {
if (cookies.get(key)) {
return cookies.get(key);
}
cookies.set(key, value);
});
const updateCookie = (value: string) => {
removeItem(value);
cookies.set(key, value);
};
const removeItem = (key: string) => {
cookies.remove(key);
};
return [cookie, updateCookie, removeItem];
};
If you notice though, it flickers on every page load. Is there a way to store and display that data differently so that it won't do this?
Thanks in advance.
So, I was able to figure out a better solution with a bit of digging. Rather than trying to debug the hook that I built, I'm using axios-hooks, which has all of the same functionality that I needed, but solves the problem. In my layout, I've got the data being fetched like so:
const [cookie, updateCookie] = useCookie('one-day-location', '1');
const [{ data, loading, error }] = useAxios(
`https://jsonplaceholder.typicode.com/users/${cookie}`,
);
And I'm then able to pass the data down to my Header component, but it doesn't load the API multiple times, and eliminates the flickering. I can absolutely add more documentation here in my answer if anyone has any more questions.
Related
I am new to react (that I use with typeScript) and I am facing an issue with the use of the useMemo hook.
Here is my fetching service:
export default class FetchingService {
datas: Data[] = [];
constructor() {
this.fetch();
}
async fetch(): Promise<Data[]> {
const d = // await an async array from an api, using Array.flat()
this.datas = d;
console.log(this.datas);
return d;
}
}
In a component, I try to watch for change of the datas attribute of my service:
import fetchingService from '../services/fetchingService.ts';
const Home: React.FC = () => {
const ds: Data[];
const [datas, setDatas] = useState(ds);
const fetchDatas = useMemo(() => {
console.log('Render datas', fetchingService.datas?.length)
setDatas(fetchingService.datas);
return fetchingService.datas;
}, [fetchingService.datas]);
return (
<ul>{datas.map(d => {
return (
<li key={d.id}>{d.id}</li>
);
</ul>
);
}
The problem I am facing is that the useMemo hook is not recompouted when the datas attribute changes within my fetchService. I am pretty sure that my FetchingService.fetch() function works because the console.log within the fetch function always display the fetched datas.
The observed behavior is that sometimes datas are well rendered (when fetch ends before rendering ...), but sometimes it isn't.
The expected one is that datas are rendered every time and only on refresh, exept when datas are modified
I also tried to put the length of the data array as a dependency in useMemo, but in both cases it doesn't work and I have a warning in my IDE, telling me it is an unnecessary dependency.
I don't really understand if it is a typescript or a specific react behavior issue. I think the reference of the datas attribute should change at the end of the fetch (or at least its length attribute ...), but tell me if I am wrong.
I do appreciate every help !
in fetchingService, when datas change, probably the dependency cannot be accepted. You can use a custom hook in stead of it.
You can use this source about useMemo: useMemo with an array dependency?
import { useState, useLayoutEffect, useCallback } from "react";
export const useFetchingService = () => {
const [fetchedData, setFetchedData] = useState([]);
const fetch = useCallback(async () => {
const d = await new Promise((res, rej) => {
setTimeout(() => {
res([1, 2, 3]);
}, 5000);
}); // await an async array from an api, using Array.flat()
setFetchedData(d);
}, []);
useLayoutEffect(() => {
fetch();
}, []);
return [fetchedData];
};
useLayoutEffect runs before rendering
using:
const [fetchData] = useFetchingService();
const fetchDatas = useMemo(async () => {
console.log("Render datas", fetchData.length);
setDatas(fetchData);
return fetchData;
}, [fetchData]);
You can also use this directly without 'datas' state.
I hope that this will be solution for you.
So I put together a codesandbox project that uses a context to store the value:
App.tsx
import React, { useState, useEffect, createContext } from "react";
import Home from "./Home";
export const DataContext = createContext({});
export default function App(props) {
const [data, setData] = useState([]);
useEffect(() => {
const get = async () => {
const d = await fetch("https://dummyjson.com/products");
const json = await d.json();
const products = json.products;
console.log(data.slice(0, 3));
setData(products);
return products;
};
get();
}, []);
return (
<div>
Some stuff here
<DataContext.Provider value={{ data, setData }}>
<Home />
</DataContext.Provider>
</div>
);
}
Home.tsx
import React, { FC, useMemo, useState, useEffect, useContext } from "react";
import { DataContext } from "./App";
import { Data, ContextDataType } from "./types";
const Home: FC = () => {
const { data, setData }: ContextDataType = useContext(DataContext);
return (
<>
<ul>
{data.map((d) => {
return (
<li key={d.id}>
{d.title}
<img
src={d.images[0]}
width="100"
height="100"
alt={d.description}
/>
</li>
);
})}
</ul>
</>
);
};
export default Home;
This was my first time using both codesandbox and typescript so I apologize for any mistakes
I am developing a React project & using AWS Amplify as a tool for serverless backend, in which I have a DynamoDB as my database in AWS Cloud.
My React app needs to detect realtime data update in DynamoDB. I am using GraphQL API from Amplify.
The GraphQL API generated by Amplify provides a subscriptions API for subscribing for data changes in real-time. I think that's exactly what I need, so I am using it:
import React, {useEffect, useState, useRef} from 'react';
import Amplify, { API, graphqlOperation } from 'aws-amplify';
import { onCreateData } from './graphql/subscriptions';
// subscribe to listen to new data creation in DynamoDB
const subscription = API.graphql(
graphqlOperation(onCreateData)
).subscribe({
next: (newData) => {
// New data is received here outside 'App' functional component,
// how can I render the data then?
console.log(`new data: ${JSON.stringify(newData)}`);
}
});
// I can't put the above subscription code inside App component, I don't know why but it just doesn't work unless I move it outside the App functional component.
function App() {
const [dataArray, setDataArray] = useState([]);
...
useEffect(() => {
fetchDataArray();
showDataArray();
return () => {
//cleanup
}
}, [dataArra])
return (<div>
<canvas ref={canvasRef}/>
</div>)
}
export default App;
Problem is the subscription for new data insertion in DynamoDB only work if I put it outside the function App(){...} as you can see above. So, when newData is received in the subscription callback next: (newData), how can I show the new data in App component?
You can see inside my App component I have state variable dataArray, the ideal solution is to update this dataArray state with newData but I think it is impossible now. So, in general I wonder how can I show the new data in my App component now?
P.S. Amplify GraphQL API doc sample code (if you see the "subscription" section) also shows that the subscription code is in the same level as imports.
We use amplify for our projects and one example we have is for a simple chat function below:
interface ChatMessagesProps {
internal?: boolean
convoId: string
}
export const ChatMessages: FC<ChatMessagesProps> = ({ internal, convoId }) => {
const classes = useStyles()
const {
appState: { user }
} = useAppState()
let [messages, setMessages] = useState<any>([])
let [currentConversationId, setConversationId] = useState<string>(convoId)
const listRef = useRef<VariableSizeProps>()
let subscription = useRef<ISubscriptionObject | null>(null)
let updateSubscription = useRef<ISubscriptionObject | null>(null)
const addNewMessage = ({ onCreateMessage }) => {
console.log(onCreateMessage)
// from here we can do whatever we want with the data within the context of this component
}
const messageUpdated = ({ onUpdateMessage }) => {
console.log(onUpdateMessage)
// from here we can do whatever we want with the data within the context of this component
}
const getConversationDetails = async () => {
subscription.current = graphQLSubscription(
onCreateMessage,
{ conversationId: currentConversationId },
addNewMessage
)
updateSubscription.current = graphQLSubscription(
onUpdateMessage,
{ conversationId: currentConversationId },
messageUpdated
)
}
useEffect(() => {
if (currentConversationId) {
getConversationDetails()
}
return () => {
subscription?.current?.unsubscribe()
updateSubscription?.current?.unsubscribe()
}
}, [currentConversationId])
return (
<div className={classes.root}>
<MessageList listRef={listRef} messages={messages} internal={internal} />
<MessageInput userId={user?.id || ''} internal={internal} conversationId={currentConversationId} />
</div>
)
}
const useStyles = makeStyles(() => ({
root: {
width: '100%',
height: '100%',
backgroundColor: 'white',
position: 'relative'
}
}))
And we use the helper graphQLSubscription
export const graphQLSubscription = (subscription: string, options: any, callback: (d: any) => void) => {
return API.graphql(graphqlOperation(subscription, options)).subscribe({
next: ({ value: { data, errors } }) => {
console.log(errors)
callback(data)
}
})
}
For your example code you would just want to store you subscription in a ref in order to be able to unsubscribe on unmount as well
import React, {useEffect, useState, useRef} from 'react';
import Amplify, { API, graphqlOperation } from 'aws-amplify';
import { onCreateData } from './graphql/subscriptions';
function App() {
const [dataArray, setDataArray] = useState([]);
const subscriptionRef = useRef()
...
useEffect(() => {
fetchDataArray();
showDataArray();
subscriptionRef.current = API.graphql(
graphqlOperation(onCreateData)
).subscribe({
next: (newData) => {
// New data is received here outside 'App' functional component,
// how can I render the data then?
console.log(`new data: ${JSON.stringify(newData)}`);
}
});
return () => {
//cleanup
subscriptionRef.current.unsubscribe()
}
}, [dataArra])
return (<div>
<canvas ref={canvasRef}/>
</div>)
}
export default App;
From there you can extract it into helper functions within the component or within you project like above
Im trying to make a system of likes for each recipe that i have on my page. The endpoint works, i basically pass the id and when i submit, it increments the number of likes by one of that recipe.
Im trying to make the frontend part for it.
Basically im building a custom hook with
import { useState, useEffect } from "react";
import axios from "axios";
const useLikes = (id) => {
const [like, setLike] = useState();
useEffect(() => {
(async (id) => {
const response = await axios.post(`
https://obscure-river-28733.herokuapp.com/recipe/like/${id}
`);
setLike(response.data);
})(like);
});
console.log("like" + like);
return like;
};
export default useLikes;
Now this is the part im a bit stuck on what to do.
Im bulding the icon like this
import React from "react";
import { BsHeart } from "react-icons/bs";
import useLikes from "./useLikes";
const Likes = ({ id }) => {
const likes = useLikes(id);
return (
<BsHeart
likes={id}
color="red"
size={18}
onClick={() => console.log(likes)}
/>
);
};
export default Likes;
And on the app component inside my map i call it like this
<Likes likes={record.id} />
id comes undifined so its not making the post to nothing viable. Could anyone help me solve this please?
Your Likes component should be defined like this:
const Likes = ({ likes: id }) => {
or change the prop name when calling it
<Likes id={record.id}/>
Api call is asynchronous so it should be done simply inside an asynchronous function
const useLikes = async (id) => {
const response = await axios.post(`
https://obscure-river-28733.herokuapp.com/recipe/like/${id}
`);
return response.data;
};
Then in the Likes component you can use the useLikes function like this
const [likes, setLikes] = useState(0);
useLikes(id).then((res) => setLikes(res));
I am stuck at getting context data.
I have a context and a component which uses its data.
I need to get the updated data of context's variable on API call success in my component.
so How can I do that ?
Here what I have tried.
context.js
import React, { useState, createContext,useEffect } from 'react';
import {getData} from './actionMethods';
const NewContext = createContext();
function newContextProvider(props) {
const [dataValue, setData] = useState([])
useEffect(() => {
const fetchMyData = async () => {
const dataValue = await getData(); // this is an API call
setData(dataValue)
};
fetchMyData();
}, []);
return (
<NewContext.Provider
value={{
state: {
dataValue
},
actions: {
}
}}
>
{props.children}
</NewContext.Provider>
);
}
const newContextConsumer = newContext.Consumer;
export { newContextProvider, newContextConsumer, newGridContext };
myComponent.js
import React, { useState, useContext } from 'react'
import context from './context'
import deleteAPI from './actionMethods'
function myComponent(props) {
const id= 10
const {state,actions} = useContext(context)
deleteAPI(id).then(res => {
if (res){
// what should I write here to get the updated Data from the context which will call an API to get the updated data.
}
})
}
Any help would be great.
Thank You.
As a generic example, one option is to fetch the data from the server when the app loads in the front-end. From there you can send requests to modify the server data and at the same time update your local version. Something like:
Fetch data and save it to the local store: [{id: 0, name: 'first'},{id: 1, name: 'second'}]
Modify the data sending a request to the server. For example deleting an item. id: 0
Once the server responds confirming the operation was successful you can modify that data in the local store. [{id: 1, name: 'second'}]
You can handle the data using a Redux store or a React Context. For example, using a Context:
export const ItemsContext = createContext([]);
export const ItemsContextProvider = props => {
const [items, setItems] = useState([]);
const deleteItem = id => {
deleteItemsAxios(id).then(() => {
setItems(items => items.filter(item => item.id !== id));
});
};
useEffect(() => {
const fetchItems = async () => {
const items_fetched = await fetchItemsAxios();
if (items_fetched) {
setItems(items_fetched);
} else {
// Something went wrong
}
};
fetchItems();
}, []);
return (
<ItemsContext.Provider
value={{
items,
deleteItem
}}
>
{props.children}
</ItemsContext.Provider>
);
};
We define a Component that will manage the data fetch. The data items are inside a state. When the Component mounts we fetch the items and save them in the state. If we want to delete an item we first call the corresponding fetch function. Once it finishes, if it was successful, we update the state and remove that item. We use React Context to pass the items data, as well as the deleteItem function, to any component that needs them.
Let me know if you need more explanation.
Stripe has react-stripe-elements, which provides injectStripe HOC. We're in 2019 and HOCs are not cool anymore. Stripe doesn't appear to be in a hurry, i assume it's because they want to support older versions of React, so are looking at lowest common denominator solutions only.
I'm looking for a way to get stripe via a hook (instead of a prop provided by injectStripe).
Usage would look like this
const stripe = useStripe()
so that later I can use stripe.handleCardSetup and other API methods
import { CardElement } from 'react-stripe-elements'
const CardForm = ({secret}) => {
const stripe = useStripe()
const handleSubmit = async () => {
const { setupIntent, error } = await stripe.handleCardSetup(secret)
// ...
}
return (
<>
<CardElement />
<button onClick={handleSubmit} />
</>
)
}
how would you define useStripe hook from existing APIs and components?
stripe should be the same as what you'd get if you used injectStripe hook
Related issue on GH: API Review: Hooks & Beyond
After a bunch of trial and error, i ended up with this, which is quite simple. Here it is for stripe peeps to adopt and for others who need useStripe hook
// StripeHookProvider.jsx
import React, { useContext, createContext } from 'react'
import { injectStripe } from 'react-stripe-elements'
const Context = createContext()
export const useStripe = () => useContext(Context)
const HookProvider = ({ children, stripe }) => {
return <Context.Provider value={stripe}>{children}</Context.Provider>
}
export default injectStripe(HookProvider)
Import and add StripeHookProvider to your component hierarchy like this. NOTE: because it relies on injectStripe, it must be a child of <Elements>
<StripeProvider {...{ stripe }}>
<Elements>
<StripeHookProvider>
<MyForm />
</StripeHookProvider>
</Elements>
</StripeProvider>
then inside MyForm you use it like this
// MyForm.jsx
import { useStripe } from './StripeHookProvider'
const MyForm = () => {
const stripe = useStripe()
const handleSubmit = () => {
const { setupIntent, error } = await stripe.handleCardSetup(secret ... // and so on
}
}
This way you only need to do injectStripe in one place and it's tucked away neatly out of sight. As you can see from this code, it's not rocket science and should be no problem for stripe peeps to add to their code, hopefully eliminating <StripeHookProvider> altogether since they have a reference to stripe object somewhere in context.
I tried a bunch of approaches and this is the only one that worked. I didn't dig through stripe's JS code to see what happens, but stripe you get from injectStripe is the only thing you can use. The one you setup via window.Stripe(apiKey) is not the same and it won't work as it doesn't observe <Elements> properly so it doesn't know the state of the form.
Perhaps, the reason injectStripe has to be used on children of <Elements> is the same reason Stripe crew is taking a while to build this hook.
I've done it myself, and I've prepared a gist for you :)
The useStripe hook uses a useScript hook that its responsible to load async scripts like the stripe one..
Here is the gist:useScript.js
I've simplified it, you may not need the locale either.
Here is the useScript:
import React from 'react'
let cache = []
const isCached = src => cache.includes(src)
export const useScript = src => {
const [state, setState] = React.useState({
isLoaded: isCached(src),
hasError: false,
})
React.useEffect(() => {
if (!src || isCached(src)) {
return
}
cache.push(src)
const script = document.createElement('script')
script.src = src
script.async = true
const onScriptLoad = () => {
setState(s => ({...s, isLoaded: true, hasError: false}))
}
const onScriptError = () => {
const index = cache.indexOf(src)
if (index >= 0) {
cache.splice(index, 1)
}
script.remove()
setState(s => ({...s, hasError: true, isLoaded: true}))
}
script.addEventListener('load', onScriptLoad)
script.addEventListener('error', onScriptError)
document.body.appendChild(script)
return () => {
script.removeEventListener('load', onScriptLoad)
script.removeEventListener('error', onScriptError)
}
}, [src])
return state
}
Here is the useStripe hook:
import React from 'react'
import {useScript} from './useScript'
let cache = {}
export const useStripe = ({locale, stripeKey}) => {
const {isLoaded, error} = useScript('https://js.stripe.com/v3/')
const [stripe, setStripe] = React.useState(cache[locale])
React.useEffect(() => {
if (isLoaded && !error && !cache[locale]) {
cache[locale] = window.Stripe(stripeKey, {locale})
setStripe(cache[locale])
}
}, [isLoaded, error, locale])
return {
stripe,
error,
}
}
This is just the hook, but if you need the context API's you can move the useStripe code inside the Provider!