How to create useStripe hook for react-stripe-elements - reactjs

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!

Related

How to run functional component in useEffect used for updating context

Trying to get the API functional component that updates Context values in useEffect to execute within another App functional component (on import) when mounted to allow render when values exist/update.
The API function at API.js appears to be working correctly as a stand alone component. The problem (due to lack of understanding) is upon the import to App.js where the API function should execute.
API.js
import { useState, useContext, useEffect } from 'react'
import { AppContext } from './contexts/AppContext';
export const API = () => {
const {updateOrder, updateCustomer} = useContext(AppContext);
useEffect(() => {
const fetchData = async () => {
const orderDetails = await fetch('url.com/order');
const customerDetails = await fetch('url.com/customer');
updateOrder(await orderDetails.json())
updateCustomer(await customerDetails .json())
}
fetchData();
}, [])
}
App.js
import React, { useContext, useEffect } from 'react';
import { AppContext } from './contexts/AppContext';
import { API } from './components/API';
const App = ({ isLoading }) => {
const {order, customer} = useContext(AppContext);
useEffect(() => {
isLoading && <API />
}, []) // eslint-disable-line react-hooks/exhaustive-deps
if (order && customer) {
return <SomeComponent/>
}
}
The expected outcome is to be able to use API within the initial mount (as API dependency) and conditionally return/render content in App.
I've tried changing the API component into function and exporting with default, however context is not supported outside of component.
A component is a function that renders something in the UI. You API component is not really a component - it doesn't have any branch that returns any JSX - although that's not always required.
You should look into building a custom hook instead. Try something like this:
App.js
import React, { useContext, useEffect } from "react";
import { AppContext } from "./contexts/AppContext";
// Hooks should start with 'use'
export const useAPI = () => {
const { updateOrder, updateCustomer } = useContext(AppContext);
useEffect(() => {
const fetchData = async () => {
// Fetching data from both endpoints in parallel
const res = await Promise.all([fetch("url.com/order"), fetch("url.com/customer")]);
// Converting both payloads to JSON in parallel
const data = await Promise.all([res[0].json(), res[1].json()]);
updateOrder(data[0]);
updateCustomer(data[1]);
};
fetchData();
}, []);
};
const App = ({ isLoading }) => {
// Calling the custom hook
useAPI();
const { order, customer } = useContext(AppContext);
if (order && customer) {
return <SomeComponent />;
}
};

React hook "useMemo" with array as dependency

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

AWS Amplify in React, how can I render the new data from the subscribed listener

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

Current user with React Context

I want to have a way to get and fetch the current user using React Context (to not pass props to all my components)
I tried using React Context but didn't understand how I would achieve something like const { currentUser, fetchCurrentUser } = useCurrentUser() from the docs.
here is what i did for my project:
// src/CurrentUserContext.js
import React from "react"
export const CurrentUserContext = React.createContext()
export const CurrentUserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = React.useState(null)
const fetchCurrentUser = async () => {
let response = await fetch("/api/users/current")
response = await response.json()
setCurrentUser(response)
}
return (
<CurrentUserContext.Provider value={{ currentUser, fetchCurrentUser }}>
{children}
</CurrentUserContext.Provider>
)
}
export const useCurrentUser = () => React.useContext(CurrentUserContext)
and then use it like this:
setting up the provider:
// ...
import { CurrentUserProvider } from "./CurrentUserContext"
// ...
const App = () => (
<CurrentUserProvider>
...
</CurrentUserProvider>
)
export default App
and using the context in components:
...
import { useCurrentUser } from "./CurrentUserContext"
const Header = () => {
const { currentUser, fetchCurrentUser } = useCurrentUser()
React.useEffect(() => fetchCurrentUser(), [])
const logout = async (e) => {
e.preventDefault()
let response = await fetchWithCsrf("/api/session", { method: "DELETE" })
fetchCurrentUser()
}
// ...
}
...
the full source code is available on github: https://github.com/dorianmarie/emojeet
and the project can be tried live at: http://emojeet.com/
If useContext returns undefined, then you might have forgotten the Provider or need to move your provider up the stack.
U dont explained what u want to do with the data but...After u exec the function fetch in use effect.
Now u have the object user in the state currentUser.
Try to console log after the use effect the currentUser and see what dat inside it.
After u can use it with currentUser."whatever prop inside"..

Data fetched with axios flickering on page load

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.

Resources