How to handle deeplinks in react - reactjs

I am trying to implement deeplinks in my app using firestore's dynamic links. When the app is opened using a deeplink, I want to show a modal to the user. I am new to react and I am not sure how to implement this. When the app is opened using the dynamic link, I can get the link in App.js
useEffect(() => {
const unsubscribe = dynamicLinks().onLink(handleDynamicLink);
return () => unsubscribe();
}, []);
const handleDynamicLink = link => {
//Show Modal
};
return (
<RootStoreProvider value={rootStore}>
<SafeAreaProvider initialSafeAreaInsets={initialWindowSafeAreaInsets}>
<IconRegistry icons={EvaIconsPack} />
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<ApplicationProvider {...eva} theme={eva[theme]}>
{!rootStore.authStore.isLoggedIn && !startedPressed ? <WelcomeSliderScreen pressed={getStartedPressed}></WelcomeSliderScreen> :
<RootNavigator
ref={navigationRef}
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
/>
}
</ApplicationProvider>
</ThemeContext.Provider>
</SafeAreaProvider>
</RootStoreProvider>
)
}
What is the best way to implement this logic? I started to implement a deepLinkStore but now a start the think that this is not the best solution
const handleDynamicLink = link => {
rootStore && rootStore.linkStore.setDeepLink(link);
};
The problem is that rootStore is sometimes null there, I don't understand why.
Shall I just provide a parameter in RootNavigator? Something like this
const [deepLink, setDeepLink] = React.useState(null)
const handleDynamicLink = (link) => {
setDeepLink(link);
}
<RootNavigator
ref={navigationRef}
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
deepLink={deepLink}
/>
Is this the way to go? How can I access the deepLink parameter in a functional component?

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'
});
}, []);
...

Jest - how to get useSelect data in tesing file

I have a component in the React project which uses 2 useSelectors to get data for rendering the page. While testing I am able to cover the UI part but the conditional UI part is not coming on, as useSelector data is not replicatable via mock. Can someone help me.
const MyComponent = () => {
const { id } = useSelector(selecetdId);
const dataArray = useSelector(data);
return (
<div>
{!id ? (
<Fragment>
<AnotherCustomComponent />
{dataArray.length > 0 ? **<CustomTable dataArray={filteredSuppliers} />** : <NoData />}
</Fragment>
) : (
**<div className="supplier-details">
<CustomHeader />
</div>**
)}
</div>
);
};
As i dont have hold on the output from the useSelector id, dataArray, In testing i am not able to cover starred part of code
I have a feeling you are using ReactTestingLibrary and redux with dedux-toolkit ;)
You need to do two things:
Have a mocked state, with a values that can result in the condition you want to test
Render the component within redux provider
Quick sample
const renderComponent(props, reduxState) {
const reduxStore = configureStore({ reducer: {} /**/, preLoadedSate: reduxState });
return render(<Provider store={reduxStore}><MyComponentToBeTested ...props /></Provider>))
}
it("Should show something if id in the state is undefined", () => {
const stateSoThatUseSelectorWillReturnIdUndefined = { john: true };
const renderedResult = renderComponent({}, stateSoThatUseSelectorWillReturnIdUndefined);
/// expect something is rendered or not
});
More details here: redux state unit testing

Use context for communication between components at different level

I'm building the settings pages of my apps, in which we have a common SettingsLayout (parent component) which is rended for all the settings page. A particularity of this layout is that it contains an ActionsBar, in which the submit/save button for persisting the data lives.
However, the content of this SettingsLayout is different for each page, as every one of them has a different form and a different way to interact with it. For persisting the data to the backend, we use an Apollo Mutation, which is called in one of the child components, that's why there is no access to the ActionsBar save button.
For this implementation, I thought React Context was the most appropriated approach. At the beginning, I thought of using a Ref, which was updated with the submit handler function in each different render to be aware of the changes.
I've implemented a codesandbox with a very small and reduced app example to try to illustrate and clarify better what I try to implement.
https://codesandbox.io/s/romantic-tdd-y8tpj8?file=/src/App.tsx
Is there any caveat with this approach?
import React from "react";
import "./styles.css";
type State = {
onSubmit?: React.MutableRefObject<() => void>;
};
type SettingsContextProviderProps = {
children: React.ReactNode;
value?: State;
};
type ContextType = State;
const SettingsContext = React.createContext<ContextType | undefined>(undefined);
export const SettingsContextProvider: React.FC<SettingsContextProviderProps> = ({
children
}) => {
const onSubmit = React.useRef(() => {});
return (
<SettingsContext.Provider value={{ onSubmit }}>
{children}
</SettingsContext.Provider>
);
};
export const useSettingsContext = (): ContextType => {
const context = React.useContext(SettingsContext);
if (typeof context === "undefined") {
/*throw new Error(
"useSettingsContext must be used within a SettingsContextProvider"
);*/
return {};
}
return context;
};
function ExampleForm() {
const { onSubmit } = useSettingsContext();
const [input1, setInput1] = React.useState("");
const [input2, setInput2] = React.useState("");
onSubmit.current = () => {
console.log({ input1, input2 });
};
return (
<div className="exampleForm">
<input
placeholder="Input 1"
onChange={(event) => setInput1(event.target.value)}
/>
<input
placeholder="Input 2"
onChange={(event) => setInput2(event.target.value)}
/>
</div>
);
}
function ActionsBar() {
const { onSubmit } = useSettingsContext();
return (
<section className="actionsBar">
<strong>SETTINGS</strong>
<button onClick={() => onSubmit?.current()}>Save</button>
</section>
);
}
export default function App() {
return (
<div className="App">
<SettingsContextProvider>
<ActionsBar />
<ExampleForm />
</SettingsContextProvider>
</div>
);
}
The main caveat I see in this approach is that you change the whole submit function when you need only reaction to submit event. Event is the catch, I think.
Your approach works ok, but has no extension points, for cases such as validation etc.
So I propose to use EventEmitter in any form (better with types support) as a context value e.g. communication channel.
This is a fork of your codesandbox that illustrates this approach:
https://codesandbox.io/s/friendly-fog-qlrusj?file=/src/App.tsx

How to share a single MUI useScrollTrigger return value among multiple components?

I am currently using MUI's useScrollTrigger hook to determine the appearance of three components - NavBar, a post FAB a back to top button e.g.:
export default function NavBar() {
const isScrolledDown = useScrollTrigger({ target: window, threshold: 100 });
return (
<>
<Slide in={!isScrolledDown} >
<AppBar>
<Toolbar>
</Toolbar>
</AppBar>
</Slide>
<Toolbar />
<BackToTopFAB isScrolledDown={isScrolledDown} />
<PostCreateFAB isScrolledDown={isScrolledDown} />
</>
);
}
Since I do not want to make the browser listen for three separate "scroll" events, I am currently drilling the hook's return value from the NavBar into the two buttons.
However, as a result, I am unable to decouple the two buttons from the NavBar.
Does anyone have any suggestions how this may be possible, so that all three components share the same hook return value? If having multiple "scroll" listeners is not DOM-intensive, I am also willing to consider that
React hook is designed to be reusable, you probably want to move the useScrollTrigger hook to the components that need it like below:
const useCustomScrollTrigger = () => useScrollTrigger({ target: window, threshold: 100 });
const BackToTopFAB = () => {
const isScrolledDown = useCustomScrollTrigger();
return (...)
}
const PostCreateFAB = () => {
const isScrolledDown = useCustomScrollTrigger();
return (...)
}
const MyAppBar = () => {
const isScrolledDown = useCustomScrollTrigger();
return (
<Slide in={!isScrolledDown} >
<AppBar />
</Slide>
)
}
export default function NavBar() {
return (
<>
<MyAppBar />
<OtherContent />
<BackToTopFAB />
<PostCreateFAB />
</>
);
}
Doing so has a couple of advantages:
Your code is easier to read because the logic is hidden away in each specific component. Code readability is one of the most important factors when choosing between trade-offs IMO. Several additional event listeners should never impact your application performance in any way.
Improve your the performance of the parent component since there is no props at the top-level component, if the isScrolledDown state is changed, only 3 isolated components are re-rendered as a result. Otherwise, other components in the page like OtherContent also need to be rendered because the state in the parent component changes.
You can also have a look at some react state management libraries like redux-toolkit if you want to store the state in a single place and access it anywhere in the components regardless of its position in the hierarchy:
import { createSlice } from '#reduxjs/toolkit'
const { actions } = createSlice({
name: 'globalState',
initialState: { isScrolledDown: false },
reducers: {
setIsScrolledDown: (state, action) => {
state.isScrolledDown = action.payload
},
},
})
const ScrollLisenter = () => {
const isScrolledDown = useScrollTrigger({ /* ... */ });
const dispatch = useDispatch()
useEffect(() => {
dispatch(actions.setIsScrolledDown(isScrolledDown));
}, [isScrolledDown]);
return null
}
const BackToTopFAB = () => {
const isScrolledDown = useSelector(state => state.globalState.isScrolledDown);
return (...)
}
const PostCreateFAB = () => {
const isScrolledDown = useSelector(state => state.globalState.isScrolledDown);
return (...)
}
<App>
<ScrollLisenter />
<NavBar />
</App>
Related Question
Does adding too many event listeners affect performance?

AWS Amplify - How to render components after sign in

I have an AWS Amplify app using React. I want to be able to only load (or reload) a TaskList component only when the user has successfully signed in. However, the component gets rendered from the very beginning when page loads and when user fills up form and gets signed up it won't reload. I have been trying multiple workarounds but I can't see how to make my component depend on a successful login. I rely on the default Amplify authenticator functions to sign the user in against Cognito.
const App = () => (
<AmplifyAuthenticator>
<div>
My App
<AmplifySignOut />
<TaskList />
</div>
</AmplifyAuthenticator>
);
I managed to solve it using hints given in this answer AWS Amplify: onStatusChange then render main page.
Basically, I changed my App component to return only sign in form or the whole up based on auth state change.
const App = () => {
const [authState, setAuthState] = useState('');
function handleAuthStateChange(state) {
if (state === 'signedin' || state === 'signedout') {
setAuthState(state);
}
}
return (
<div>
{ authState !== 'signedin' ?
<AmplifyAuthenticator>
<AmplifySignIn handleAuthStateChange={handleAuthStateChange} slot="sign-in"></AmplifySignIn>
</AmplifyAuthenticator>
:
<div>
My App
<AmplifySignOut handleAuthStateChange={handleAuthStateChange} slot="sign-out"/>
<TaskList />
</div>
}
</div>
);
}
This is how I solved a similar issue to manage the states. I was having some problems as it didn't seem to dispatch the events afterwards.
From https://github.com/aws-amplify/amplify-js/issues/5825
import React from 'react';
import { AmplifyAuthenticator, AmplifySignOut, AmplifySignUp, AmplifySignIn} from '#aws-amplify/ui-react';
import { onAuthUIStateChange } from '#aws-amplify/ui-components'
const Panel = () => {
const [setAuthState] = React.useState();
React.useEffect(() => {
return onAuthUIStateChange(newAuthState => {
if(newAuthState === 'signedin'){
// Do your stuff
}
setAuthState(newAuthState)
});
}, []);
return(
<AmplifyAuthenticator>
<AmplifySignIn headerText="Sign In" slot="sign-in"/>
<AmplifySignUp slot="sign-up" formFields={[
{type: 'username'},
{type: 'email'},
{type: 'password'}
]}></AmplifySignUp>
<AmplifySignOut></AmplifySignOut>
</AmplifyAuthenticator>
)
}
export default Panel;

Resources