I'm trying to integrate CleverTap into my Next.js app. Followed the documentation Web SDK Quick Start Guide but facing issue:
Server Error ReferenceError: window is not defined in Next.js
_app.tsx
import React, { useEffect, useState } from "react";
import type { AppProps } from "next/app";
import { appWithTranslation } from "next-i18next";
import { Hydrate, QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
import nextI18NextConfig from "../next-i18next.config.js";
import "tailwindcss/tailwind.css";
import "styles/globals.scss";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import { useRouter } from "next/router";
import SvgPageLoading from "components/color-icons/PageLoading";
// import { PageLoading } from "components/color-icons/";
import { DefaultSeo } from 'next-seo';
import SEO from 'next-seo.config';
import {cleverTap} from "utils/cleverTapHelper";
cleverTap.initialize('TEST-61c-a12');
function MyApp({ Component, pageProps }: AppProps) {
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: Infinity,
},
},
})
);
const router = useRouter();
const [isAnimating, setIsAnimating] = useState(false);
useEffect(() => {
const handleStart = () => {
setIsAnimating(true);
};
const handleStop = () => {
setIsAnimating(false);
};
router.events.on("routeChangeStart", handleStart);
router.events.on("routeChangeComplete", handleStop);
router.events.on("routeChangeError", handleStop);
return () => {
router.events.off("routeChangeStart", handleStart);
router.events.off("routeChangeComplete", handleStop);
router.events.off("routeChangeError", handleStop);
};
}, [router]);
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<DefaultSeo {...SEO} />
<Component {...pageProps} />
{isAnimating && (
<div className="fixed top-0 left-0 flex items-center justify-center w-screen h-screen overflow-visible bg-white bg-opacity-80 z-overlay top-z-index">
<SvgPageLoading />
</div>
)}
<ReactQueryDevtools initialIsOpen={false} />
</Hydrate>
</QueryClientProvider>
);
}
export default appWithTranslation(MyApp, nextI18NextConfig);
cleverTapHelper.ts
export const cleverTap = {
initialize: function (accountId) {
console.log('I see initialize req')
window.clevertap = {event: [], profile: [], account: [], onUserLogin: [], notifications: []};
window.clevertap.account.push({'id': accountId});
(function () {
var wzrk = document.createElement('script');
wzrk.type = 'text/javascript';
wzrk.async = true;
wzrk.src = ('https:' == document.location.protocol ? 'https://d2r1yp2w7bby2u.cloudfront.net' : 'http://static.clevertap.com') + '/js/a.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wzrk, s);
})();
},
event: function (name, payload = {}) {
console.log('I see event req')
if (payload) {
window.clevertap.event.push(name, payload);
} else {
window.clevertap.event.push(name);
}
},
profile: function (payload) {
console.log('I see profile req')
window.clevertap.profile.push(payload);
},
logout: function () {
console.log('I see logout req')
window.clevertap.logout();
}
};
cleverTap.d.ts
declare global {
interface Window {
clevertap: any;
}
}
export {};
Window object should not be undefined but getting undefined! What's going on?
This is because NextJS is trying to execute that function on the server because it uses SSR, and window is a browser object. Since the window object is available only in the browser (client-side), the server is unable to identify the window object, hence getting undefined. In order to fix this, you should make sure that any functions/components that contain client-side related code be executed only on the browser or client-side. One way is using hooks such as useEffect that run only after the component is mounted. Another way is to use lazy loading which pretty much does the same thing.
Using useEffect hook.
In your _app.tsx component, add a new useEffect hook and move the initialization code into the newly created useEffect function.
useEffect(()=>{
cleverTap.initialize('TEST-61c-a12');
},[])
Using lazy loading. (Dynamic import)
Instead of directly importing the function, import it dynamically and set server-side rendering to false:
import dynamic from 'next/dynamic'
const cleverTap = dynamic(()=>{
return import("utils/cleverTapHelper")
},
{ssr:false}
)
cleverTap.initialize('TEST-61c-a12');
For TS folks struggling out there with clevertap and nextjs, install sdk and types:
npm i -S clevertap-web-sdk #types/clevertap-web-sdk
then, async import while initializing:
import CleverTap from 'clevertap-web-sdk/clevertap';
// ^^ this only imports types
let clevertap: CleverTap;
export const initClevertap = async (clevertapAccountID: string, region: string): Promise<void> => {
clevertap = (await import('clevertap-web-sdk')).default;
clevertap.init(clevertapAccountID, region);
clevertap.privacy.push({ optOut: false });
clevertap.privacy.push({ useIP: true });
clevertap.setLogLevel(0);
clevertap.spa = true;
};
PS: dynamic import didn't worked for me, looks like it's only for components and not libs
Related
Next JS 13 was just released last month and totally changed the way data was fetched while also providing an alternative to the use of _app.js and _document.js with the use of the root layout.js. Previously in Next JS 12 and below, to use the React Query SSR feature using the Hydration method, you needed to set your _app.js file like this:
import { Hydrate, QueryClient, QueryClientProvider } from '#tanstack/react-query';
import queryClientConfig from '../queryClientConfig';
export default function MyApp({ Component, pageProps }) {
const queryClient = useRef(new QueryClient(queryClientConfig));
const [mounted, setMounted] = useState(false);
const getLayout = Component.getLayout || ((page) => page);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<ErrorBoundary FallbackComponent={ErrorFallbackComponent}>
<QueryClientProvider client={queryClient.current}>
<Hydrate state={pageProps.dehydratedState}>
<AppProvider>
{getLayout(<Component {...pageProps} />)}
</AppProvider>
</Hydrate>
</QueryClientProvider>
</ErrorBoundary>
);
}
To utilize React Query SSR in a page in Next JS using getServerSideProps, it goes like this:
// Packages
import Head from 'next/head';
import { dehydrate, QueryClient } from '#tanstack/react-query';
// Layout
import getDashboardLayout from '../../layouts/dashboard';
// Parse Cookies
import parseCookies from '../../libs/parseCookies';
// Hooks
import { useFetchUserProfile } from '../../hooks/user';
import { fetchUserProfile } from '../../hooks/user/api';
import { getGoogleAuthUrlForNewAccount } from '../../hooks/auth/api';
import { fetchCalendarsOnServer } from '../../hooks/event/api';
import { useFetchCalendars } from '../../hooks/event';
// Store
import useStaticStore from '../../store/staticStore';
// `getServerSideProps function`
export async function getServerSideProps({ req, res }) {
const cookies = parseCookies(req);
const queryClient = new QueryClient();
try {
await queryClient.prefetchQuery(['fetchUserProfile'], () =>
fetchUserProfile(cookies.userAccessToken)
);
await queryClient.prefetchQuery(['fetchCalendars'], () => fetchCalendarsOnServer(cookies.userAccessToken));
await queryClient.prefetchQuery(['getGoogleAuthUrlForNewAccount'], () =>
getGoogleAuthUrlForNewAccount(cookies.userAccessToken)
);
} catch (error) {
}
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
}
function Home() {
const {
data: userProfileData, // This data is immediately made available without any loading as a result of the hydration and fetching that has occurred in `getServerSideProps`
isLoading: isUserProfileDataLoading,
error: userProfileDataError,
} = useFetchUserProfile();
const { data: savedCalendarsData } = useFetchCalendars(); // This data is immediately made available without any loading as a result of the hydration and fetching that has occurred in `getServerSideProps`
return (
<>
<Head>
<title>
{userProfileData.data.firstName} {userProfileData.data.lastName} Dashboard
</title>
<meta
name="description"
content={`${userProfileData.data.firstName} ${userProfileData.data.lastName} Dashboard`}
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<PageContentWrapper
>
Page Content
</PageContentWrapper>
</>
);
}
Home.getLayout = getDashboardLayout; // This layout also needs data from userProfileData to be available. There is no problem and it never loads because the data is immediately available on mount.
export default Home;
Here is the old DashboardLayout component:
// Packages
import PropTypes from 'prop-types';
// Hooks
import { useFetchUserProfile } from '../../hooks/user';
DashboardLayout.propTypes = {
children: PropTypes.node.isRequired,
};
function DashboardLayout({ children }) {
const { isLoading, error, data: userProfileData } = useFetchUserProfile(); // Data is immediately available and never loads because it has been fetched using SSR in getServerSideProps
if (isLoading)
return (
<div className="w-screen h-screen flex items-center justify-center text-white text-3xl font-medium">
Loading...
</div>
);
if (error) {
return (
<div className="w-screen h-screen flex items-center justify-center text-brand-red-300 text-3xl font-medium">
{error.message}
</div>
);
}
return (
<>
<div className="the-dashboard-layout">
{/* Start of Main Page */}
<p className="mb-2 text-brand-gray-300 text-sm leading-5 font-normal">
<span className="capitalize">{`${userProfileData.data.firstName}'s`}</span> Layout
</p>
</div>
</>
);
}
export default function getDashboardLayout(page) {
return <DashboardLayout>{page}</DashboardLayout>;
}
Using the new Next JS 13, there is no way to use the React Query Hydration method and even when I am able to fetch the data using the new method, the data is still refetched when the component mounts which causes the layout to be in a loading state as the data is not immediately available.
In Next 13, you only need to call the data fetching method and pass it to the client components directly because the app directory now supports server components directly.
First of all, a root layout file replaces the old _app.js and _document.js file in Next 13: It is important to note that there is no pageProps object anymore for the dehydratedState.
Here is the RootLayout server component:
// Packages
import PropTypes from 'prop-types';
// Components
import RootLayoutClient from './root-layout-client';
RootLayout.propTypes = {
children: PropTypes.node.isRequired,
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<RootLayoutClient>{children}</RootLayoutClient>
</body>
</html>
);
}
Here is the RootLayoutClient client component for the layout needed because of the use of Context and State which are client-side operations:
'use client';
// Packages
import React, { useRef, useEffect } from 'react';
import { QueryClient, QueryClientProvider } from '#tanstack/react-query';
import PropTypes from 'prop-types';
// Context
import { AppProvider } from '../contexts/AppContext';
// Config
import queryClientConfig from '../queryClientConfig';
RootLayoutClient.propTypes = {
children: PropTypes.node.isRequired,
};
export default function RootLayoutClient({ children }) {
const queryClient = useRef(new QueryClient(queryClientConfig));
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<QueryClientProvider client={queryClient.current}>
<AppProvider>
{children}
</AppProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
getServerSideProps method has now been replaced with normal Promise based data fetching method using the FETCH API. The fetched data returned can now passed into the page/component that needs it.
Here is my data fetching function:
import { getCookie } from 'cookies-next';
export const fetchUserProfile = async (token) => {
if (token) {
try {
const response = await fetch(process.env.NEXT_PUBLIC_EXTERNAL_API_URL + FETCH_USER_PROFILE_URL, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
return data;
} else {
Promise.reject(response);
}
} catch (error) {
Promise.reject(error);
}
} else {
try {
const response = await fetch(process.env.NEXT_PUBLIC_EXTERNAL_API_URL + FETCH_USER_PROFILE_URL, {
method: 'GET',
headers: {
Authorization: `Bearer ${getCookie('userAccessToken')}`,
},
});
if (response.ok) {
const data = await response.json();
return data;
} else {
Promise.reject(response);
}
} catch (error) {
Promise.reject(error);
}
}
};
Here is how the data is fetched and used in the home page. Note that the home page resides in the app directory: home/page.js:
import { cookies } from 'next/headers';
// Hooks
import { fetchUserProfile } from '../../../hooks/user/api';
// Components
import HomeClient from './home-client';
export default async function Page() {
const nextCookies = cookies();
const userAccessToken = nextCookies.get('accessToken');
const userProfileData = await fetchUserProfile(userAccessToken.value);
// This is essentially prop passing which was not needed using the previous hydration and getServerSideProps methods.
// Now, I have to pass this data down to a client component called `HomeClient` that needs the data. This is done because I may need to perform some client-side operations on the component.
return <HomeClient userData={userProfileData} />;
}
Here is the HomeClient client component:
'use client';
import { useEffect } from 'react';
import PropTypes from 'prop-types';
// Hooks
import { useFetchUserProfile } from '../../hooks/user';
HomeClient.propTypes = {
userData: PropTypes.any.isRequired,
calendarData: PropTypes.any.isRequired,
};
export default function HomeClient({ userData }) {
const { isLoading, error, data: userProfileData } = useFetchUserProfile();
useEffect(() => {
console.log(JSON.stringify(userData));
}, [userData]);
// This now loads instead of being immediately available. This can be mitigated by directly using the userData passed
// through props but I don't want to engage in prop drilling in case I need it to be passed into deeper nested child components
if (isLoading) {
return (
<div>Loading...</div>
)
}
return (
<>
<AnotherChildComponent profileData={userProfileData.data.profile}/>
</>
);
}
Here is the useFetchUserProfile hook function used in HomeClient client component above:
export const useFetchUserProfile = (conditional = true) => {
// Used to be immediately available as a result of the key 'fetchUserProfile' being used to fetch data on getServerSideProps but that's not available in the app directory
return useQuery(['fetchUserProfile'], () => fetchUserProfile(), {
enabled: conditional,
cacheTime: 1000 * 60 * 5,
});
};
Here is the parent layout.js file required by NextJS 13 to share a common layout. This layout.js also needs the fetched data but there is no way to pass the data to this even through props. In the past, Data was immediately available here because of the react-query hydration performed in getServerSideProps
// Packages
import PropTypes from 'prop-types';
// Hooks
import { useFetchUserProfile } from '../../hooks/user';
DashboardLayout.propTypes = {
children: PropTypes.node.isRequired,
};
function DashboardLayout({ children }) {
const { isLoading, error, data: userProfileData } = useFetchUserProfile();
// Used to be that data was immediately available and never loaded because it has been fetched using SSR in getServerSideProps
// Now, it has to load the same data. This is even more complex because props can't be passed as there is no way or any abstraction method
// to share data between the layout and child components
if (isLoading)
return (
<div className="w-screen h-screen flex items-center justify-center text-white text-3xl font-medium">
Loading...
</div>
);
if (error) {
return (
<div className="w-screen h-screen flex items-center justify-center text-brand-red-300 text-3xl font-medium">
{error.message}
</div>
);
}
return (
<>
<div className="the-dashboard-layout">
{/* Start of Main Page */}
<p className="mb-2 text-brand-gray-300 text-sm leading-5 font-normal">
<span className="capitalize">{`${userProfileData.data.firstName}'s`}</span> Layout
</p>
</div>
</>
);
}
How can I de-duplicate the multiple requests being made and make the data available in all components that fetch the same data without prop-drilling?
And how do I also get around the limitation of not being able to pass the data to the parent layout components even if I wanted to use props.
Thank you in advance.
I am trying to import grapesjs in a nextjs project and I get the error TypeError: Cannot read properties of null (reading 'querySelector')
This seems to be that grapesjs wants to target the "#gjs" container referenced through it's id in order to load the editor inside, and it cannot find the corresponding element as the DOM is not rendered yet.
This is the code in my Editor.js component
import React, { useEffect, useState } from "react";
import grapesjs from "grapesjs";
const Editor = () => {
const [editor, setEditor] = useState(null);
useEffect(() => {
const editor = grapesjs.init({
container: "#gjs",
});
setEditor(editor);
}, []);
return (
<>
<div id="gjs"></div>
</>
);
};
export default Editor;
This is how I try to render the Editor component in the corresponding page for "/editor" route
import { getSession } from "next-auth/react";
import "../i18n/config/config";
import "grapesjs/dist/css/grapes.min.css";
import dynamic from "next/dynamic";
import Editor from "../features/Editor/components/Editor";
// const EditorComponent = dynamic(
// () => import("../features/Editor/components/Editor"),
// {
// ssr: false,
// }
// );
export default function Home() {
return <Editor />;
}
export async function getServerSideProps(context) {
const session = await getSession(context);
return {
props: {
session,
},
};
}
As you can see from the commented section, I have tried to dynamically import the editor component as I have seen this as a fix for alot of issues where an element could not be found because the DOM was not yet loaded, but it does not seem to work for me.
Edit: Adding <script src="//unpkg.com/grapesjs"></script> before the component to be rendered either in Editor.js component or in editor.js page while removing the grapejs import statement from Editor.js component import grapesjs from "grapesjs" allows the application to run but I still get the error in the console.
import { getSession } from "next-auth/react";
import "../i18n/config/config";
import "grapesjs/dist/css/grapes.min.css";
import dynamic from "next/dynamic";
import Editor from "../features/Editor/components/Editor";
// const EditorComponent = dynamic(
// () => import("../features/Editor/components/Editor"),
// {
// ssr: false,
// }
// );
export default function Home() {
return (
<>
<script src="//unpkg.com/grapesjs"></script>
<Editor />
</>
);
}
export async function getServerSideProps(context) {
const session = await getSession(context);
return {
props: {
session,
},
};
}
Edit: Screenshot of the produced error
Because it is wrong type in useState. U can try this.
const Editor = () => {
const [editor, setEditor] = useState<Record<string, any> | null>(null);
useEffect(() => {
const editor = grapesjs.init({
container: "#gjs",
});
setEditor(editor);
}, []);
return (
<>
<div id="gjs"></div>
</>
);
};
export default Editor;
Search the library sources..
I'm still new to React so forgive me if this is a silly approach to this problem.
My goal: Global error handling using a context provider and a custom hook.
The Problem: I can't remove errors without them immediately being re-added.
I display my errors via this component in the shell...
import React, { useState, useEffect } from 'react'
import Alert from '#mui/material/Alert'
import Collapse from '#mui/material/Collapse'
import { useAlertContext } from '#/context/alert-context/alert-context'
export default function AppAlert () {
const [show, setShow] = useState(false)
const alertContext = useAlertContext()
const handleClose = () => {
alertContext.remove()
setShow(false)
}
useEffect(() => {
if (alertContext.alert) {
setShow(true)
}
}, [alertContext.alert])
return (
<Collapse in={show}>
<Alert severity='error' onClose={handleClose}>
{alertContext.alert}
</Alert>
</Collapse>
)
}
I have a provider setup that also exposes a custom hook...
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
And finally I have a hook setup to hit an API and call throw errors if it any occur while fetching the data. I'm purposely triggering a 404 by passing a bad API path.
import { useEffect } from 'react'
import { useQuery } from 'react-query'
import ApiV4 from '#/services/api/v4/base'
import { useAlertContext } from '#/context/alert-context/alert-context'
export const useAccess = () => {
const alertContext = useAlertContext()
const route = '/accessx'
const query = useQuery(route, async () => await ApiV4.get(route), {
retry: 0
})
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
return query
}
This code seems to be the issue. Because alertContext.remove() triggers useEffect here and query.error still exists, it immediately re-adds the error to the page on remove. Removing alertContext from the array works, but it is not a real fix and linter yells.
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
This is a perfectly fine approach to the problem. You've also accurately identified the problem. The solution is to create a second hook with access to the methods that will modify the context. AppAlert needs access to the data in the context, and needs to update when AlertContext.alert changes. UseAccess only needs to be able to call AlertContext.add, and that method wont change and trigger a re-render. This can be done with a second Context. You can just expose one Provider and bake the actions provider into the outer context provider.
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertContextActions = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{ alert }}>
<AlertContextActions.Provider value={{ addAlert, removeAlert }}>
{children}
</AlertContextActions.Provider>
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
Now, where you need access to the alert you use one hook and where you need access to the actions you use the other.
// in AppAlert
import { useAlertContext, useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { alert } = useAlertContext()
const { removeAlert } = useAlertContextActions()
And finally
// in useAccess
import { useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { addAlert } = useAlertContextActions()
So I found a solution that seems to work for my purposes. I got a hint from this article. https://mortenbarklund.com/blog/react-architecture-provider-pattern/
Note the use of useCallback above. It ensures minimal re-renders of components using this context, as the function is guaranteed to be stable (as its memoized without dependencies).
So with this I tried the following and it solved the problem.
import React, { useState, createContext, useContext, useCallback } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = useCallback(() => setAlert(null), [])
const addAlert = useCallback((message) => setAlert(message), [])
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
My goal: Global error handling
One problem with the above useEffect approach is that every invocation of useAccess will run their own effects. So if you have useAccess twice on the page, and it fails, you will get two alerts, so it's not really "global".
I would encourage you to look into the global callbacks on the QueryCache in react-query. They are made for this exact use-case: To globally handle errors. Note that to use context, you would need to create the queryClient inside the Application, and make it "stable" with either useRef or useState:
function App() {
const alertContext = useAlertContext()
const [queryClient] = React.useState(() => new QueryClient({
queryCache: new QueryCache({
onError: (error) =>
alertContext.add(error.toString())
}),
}))
return (
<QueryClientProvider client={queryClient}>
<RestOfMyApp />
</QueryClientProvider>
)
}
I also have some examples in my blog.
Very simple app, I'm trying to display content from my API using Mobx and Axios, here's my Axios agent.ts:
import { ITutorialUnit } from './../model/unit';
import axios, { AxiosResponse } from "axios";
//set the base URL
axios.defaults.baseURL = "http://localhost:5000/api";
//store our request in a const
const responseBody = (response: AxiosResponse) => response.data;
const requests = {
get: (url: string) => axios.get(url).then(responseBody),
};
//create a const for our activty's feature,all our activities' request are go inside our Activities object
const TutorialUnits = {
list: ():Promise<ITutorialUnit[]> => requests.get("/tutorialunits"),
};
export default{
TutorialUnits
}
then I call this agent.s in a store:
import { ITutorialUnit } from "./../model/unit";
import { action, observable } from "mobx";
import { createContext } from "react";
import agent from "../api/agent";
class UnitStore {
#observable units: ITutorialUnit[] = [];
//observable for loading indicator
#observable loadingInitial = false;
#action loadUnits = async () => {
//start the loading indicator
this.loadingInitial = true;
try {
//we use await to block anything block anything below list() method
const units = await agent.TutorialUnits.list();
units.forEach((unit) => {
this.units.push(unit);
// console.log(units);
});
this.loadingInitial = false;
} catch (error) {
console.log(error);
this.loadingInitial = false;
}
};
}
export default createContext(new UnitStore());
then I call this in my App component:
import React, { Fragment, useContext, useEffect } from "react";
import { Container } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
import NavBar from "../../features/nav/NavBar";
import { ActivityDashboard } from "../../features/Units/dashboard/tutorialUnitDashboard";
import UnitStore from "../stores/unitStore";
import { observer } from "mobx-react-lite";
import { LoadingComponent } from "./LoadingComponent";
const App = () => {
const unitStore = useContext(UnitStore);
useEffect(() => {
unitStore.loadUnits();
//need to specify the dependencies in dependenciy array below
}, [unitStore]);
//we are also observing loading initial below
if (unitStore.loadingInitial) {
return <LoadingComponent content="Loading contents..." />;
}
return (
<Fragment>
<NavBar />
<Container style={{ marginTop: "7em" }}>
<ActivityDashboard />
</Container>
</Fragment>
);
};
export default observer(App);
Finally, I want to use this component to display my content:
import { observer } from "mobx-react-lite";
import React, { Fragment, useContext } from "react";
import { Button, Item, Label, Segment } from "semantic-ui-react";
import UnitStore from "../../../app/stores/unitStore";
const UnitList: React.FC = () => {
const unitStore = useContext(UnitStore);
const { units } = unitStore;
console.log(units)
return (
<Fragment>
{units.map((unit) => (
<h2>{unit.content}</h2>
))}
</Fragment>
);
};
export default observer(UnitList);
I can't see the units..
Where's the problem? My API is working, I tested with Postman.
Thanks!!
If you were using MobX 6 then you now need to use makeObservable method inside constructor to achieve same functionality with decorators as before:
class UnitStore {
#observable units: ITutorialUnit[] = [];
#observable loadingInitial = false;
constructor() {
// Just call it here
makeObservable(this);
}
// other code
}
Although there is new thing that will probably allow you to drop decorators altogether, makeAutoObservable:
class UnitStore {
// Don't need decorators now anywhere
units: ITutorialUnit[] = [];
loadingInitial = false;
constructor() {
// Just call it here
makeAutoObservable(this);
}
// other code
}
More info here: https://mobx.js.org/react-integration.html
the problem seems to be the version, I downgraded my Mobx to 5.10.1 and my mobx-react-lite to 1.4.1 then Boom everything's fine now.
I am developing a website in which I want to be able to access the state information anywhere in the app. I have tried several ways of implementing state but I always get following error message:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
Check the render method of SOS.
Here is my SOS->index.js file:
import React, { useContext } from 'react';
import axios from 'axios';
import CONST from '../utils/Constants';
import { Grid, Box, Container } from '#material-ui/core';
import { styled } from '#material-ui/styles';
import { Header } from '../Layout';
import ListItem from './ListItem';
import SOSButton from './SOSButton';
import FormPersonType from './FormPersonType';
import FormEmergencyType from './FormEmergencyType';
import StateContext from '../App';
import Context from '../Context';
export default function SOS() {
const { componentType, setComponentType } = useContext(Context);
const timerOn = false;
//'type_of_person',
const ambulance = false;
const fire_service = false;
const police = false;
const car_service = false;
//static contextType = StateContext;
const showSettings = event => {
event.preventDefault();
};
const handleComponentType = e => {
console.log(e);
//this.setState({ componentType: 'type_of_emergency' });
setComponentType('type_of_emergency');
};
const handleEmergencyType = new_emergency_state => {
console.log(new_emergency_state);
// this.setState(new_emergency_state);
};
const onSubmit = e => {
console.log('in OnSubmit');
axios
.post(CONST.URL + 'emergency/create', {
id: 1,
data: this.state //TODO
})
.then(res => {
console.log(res);
console.log(res.data);
})
.catch(err => {
console.log(err);
});
};
let component;
if (componentType == 'type_of_person') {
component = (
<FormPersonType handleComponentType={this.handleComponentType} />
);
} else if (componentType == 'type_of_emergency') {
component = (
<FormEmergencyType
handleComponentType={this.handleComponentType}
handleEmergencyType={this.handleEmergencyType}
emergencyTypes={this.state}
timerStart={this.timerStart}
onSubmit={this.onSubmit}
/>
);
}
return (
<React.Fragment>
<Header title="Send out SOS" />
<StateContext.Provider value="type_of_person" />
<Container component="main" maxWidth="sm">
{component}
</Container>
{/*component = (
<HorizontalNonLinearStepWithError
handleComponentType={this.handleComponentType}
/>*/}
</React.Fragment>
);
}
I would really appreciate your help!
Just for reference, the Context file is defined as follows:
import React, { useState } from 'react';
export const Context = React.createContext();
const ContextProvider = props => {
const [componentType, setComponentType] = useState('');
setComponentType = 'type_of_person';
//const [storedNumber, setStoredNumber] = useState('');
//const [functionType, setFunctionType] = useState('');
return (
<Context.Provider
value={{
componentType,
setComponentType
}}
>
{props.children}
</Context.Provider>
);
};
export default ContextProvider;
EDIT: I have changed my code according to your suggestions (updated above). But now I get following error:
TypeError: Cannot read property 'componentType' of undefined
Context is not the default export from your ../Context file so you have to import it as:
import { Context } from '../Context';
Otherwise, it's trying to import your Context.Provider component.
For your file structure/naming, the proper usage is:
// Main app file (for example)
// Wraps your application in the context provider so you can access it anywhere in MyApp
import ContextProvider from '../Context'
export default () => {
return (
<ContextProvider>
<MyApp />
</ContextProvider>
)
}
// File where you want to use the context
import React, { useContext } from 'react'
import { Context } from '../Context'
export default () => {
const myCtx = useContext(Context)
return (
<div>
Got this value - { myCtx.someValue } - from context
</div>
)
}
And for godsakes...rename your Context file, provider, and everything in there to something more explicit. I got confused even writing this.