I am writing a react component in nextjs that needs some information about the browser environment before it can be rendered. I know I can get this information from the user agent string after page load with a useEffect like so...
const MyComponent = () => {
const [browser, setBrowser] = useState(null);
useEffect(() => {
const ua = utils.getBrowserFromUA()
setBrowser(ua)
});
if (!browser) {
return <div>Loading...</div>
}
return (
<div>
<SpecialComponent browser={browser} />
</div>
);
};
Is it possible to get these values before the react component renders, before the pageLoad event perhaps? I really only need to calculate these values once and potentially share the values with other components. I can't find anything in the docs apart from running scripts with the beforeInteractive flag but I'm not sure how I would get the data to my component.
I have a Modal that is triggered but many different buttons across different components. I Have been able to get it working on the pages by passing the variables in the Layout, for example
const index = () => {
const [show, setShow] = useState(false)
const handleShow = () => { setShow(true) }
const handleClose = () => { setShow(false) }
<Layout pageTitle={pageTitle} metaUrl={metaUrl} show={show} onHide={handleHide}>
...
</Layout>
}
And using these variables to pass to the Modal component from the Layout which triggers the modal just fine. But it only works with pages because I can pass them in the layout, however I have buttons in the Navbar and Footer that are being imported into the Layout and not using the Layout so im not sure how to actually pass the variables to the modal from those.
Any Help would be amazing!
for this situation, I think the perfect solution would be to use React Context you could separate it in its own hook and then use this hook when needed across the app.
first, you will need to create the context
const ModalContext = React.createContext()
// give it a display name so it would be easier to debug
ModalContext.dispalyName = 'ModalContext'
then you need need to create the Provider for this context and put it higher in your app tree because you can only use this context under its Provider since you are using Next.js I would suggest doing it in the _app.js or just around your main app component.
const ModalContextProvider = ({children}) => {
const [isOpend, setIsOpend] = React.useState(false);
// handle any additional data needed with useState/useReducer
const [title, setTitle] = React.useState('default title');
const value = {setIsOpened, setTitle};
return <ModalContext.Provider value={value}>
<ModalComponent isOpend={isOpend} title={title}/>
{children}
</ModalContext.Provider>
}
so after creating the ModalContext component and putting it above your main app component you can extract this context functionality in its own hook like this
function useModalContext() {
const context = React.useContext(ModalContext);
// if context is undefined this means it was used outside of its provider
// you can throw an error telling that to your fellow developers
if(!context) {
throw new Error('useModalContext must be used under <ModalContextProvider/>');
}
return context;
}
then in any component, you can use this hook like this
const {setIsOpened, setTitle} = useModalContext();
const handleOpenModal() {
setIsOpened(true);
setTitle('Modal Title');
}
return <button onClick={handleOpenModal}>Show Modal</button>
and you can use this any place in the app if it was under the context provider component.
I am new to react and this is more of a design question where I intend to
1) Download Components from static hosted site and display them.
2) Get all data from Components through (lets say) a download option
Based on my practice, I learnt that parent component passes a callback/onChangehandle to child as props and it maintains a copy of the state of child components though child components render the HTML. However, I want to dynamically download components from API/Static hosting based on user's input and then get a string representation/state of data from all these components. Each component would have different state data and thus I cannot write a common onChangehandle.
Question:-
1) Is such a design possible with react ? Will each component be able to upload its lib to cloud ?
2) Can we enforce methods to be written on each component so that parent can call each component's specific method and get the data ?
Pseudo Code I am looking for:-
class Parent extends Component {
downloadData = (event) => {
for(childComponents 1 to n){
all_data += childComponent.getState()
}
pdf(all_data)
}
render(){
// Render Child components
for(childComponents 1 to n) {
ChildComponent = // Load from API/Static hosting
}
<button onClick="downloadData"/>
}
}
What you want to accomplish is definitely possible. I am not really into class components so I wrote the example with react hooks.
What you can do to load data from an external component is the following: Pass down a callback into the component which you call on a successful load to receive the data. Nevertheless I would not suggest to do so. I would fetch the data from a different api than the components. You should always split data to visualise and components which represent the user interface.
Code to load an external react module:
import React, { useEffect, useState, useRef } from 'react'
export function Loader({ id }) {
const [loading, setLoading] = useState(true)
const ref = useRef()
const [data, setData] = useState({})
useEffect(() => {
async function fetchReactComponent() {
await import(
/*webpackIgnore: true*/ `https://example.com/react-components/${id}`
).then(module => {
setLoading(false)
const node = module["ReactComponent"](data, setData)
if (ref.current) {
ref.current.innerHTML = ''
if (typeof node === 'string') {
ref.current.innerHTML = node
} else {
ref.current.appendChild(node)
}
}
})
}
fetchReactComponent()
// eslint-disable-next-line
}, [id])
return loading ? <div>Loading...</div> : <div ref={ref} />
}
External React module:
function ReactComponent(data, setData) {
// do something with the data
return '<h1>A component</h1>'
}
export { ReactComponent }
I'm currently migrating to antd, and have a modal appear on a certain route (ie /userid/info). I'm able to achieve this if I use the antd Modal react component, but I'd like to be able to use the modal methods provided such as Modal.confirm,Modal.info and Modal.error as they offer nicer ui straight out of the box.
I'm running to multiple issues such as having the modal rendered multiple times (both initially and after pressing delete in the delete user case), and unable to make it change due to state (ie display loading bar until data arrives). This is what i've tried but it constantly renders new modals, ive tried something else but that never changed out of displaying <Loader /> even though isFetching was false. I'm not sure what else to try.
const UserInfoFC: React.FC<Props> = (props) => {
const user = props.user.id;
const [isFetching, setIsFetching] = React.useState<boolean>(true);
const [userInfo, setUserInfo] = React.useState<string>('');
const modal = Modal.info({
content: <Loader />,
title: 'User Info',
});
const displayModal = () => {
const renderInfo = (
<React.Fragment>
<p>display user.info</p>
</React.Fragment>
);
const fetchInfo = async () => {
try {
user = // some api calls
setUserInfo(user.info);
modal.update({ content: renderInfo })
} catch (error) {
// todo
}
setIsFetching(false);
};
fetchInfo();
};
displayModal();
return(<div />);
};
reference: https://ant.design/components/modal/#components-modal-demo-confirm
edit: here is a replication of one of the issues I face: https://codesandbox.io/embed/antd-reproduction-template-1jsy8
As mentioned in my comment, you can use a useEffect hook with an empty dependency array to run a function once when the component mounts. You can initiate an async call, wait for it to resolve and store the data in your state, and launch a modal with a second hook once the data arrives.
I made a sandbox here
Instead of going to /:id/info and routing to a component which would have returned an empty div but displayed a modal, I created a displayInfo component that displays a button and that controls the modal. I got rid of attempting to use routes for this.
What I have now is similar to the docs
What is the best way to share some global values and functions in react?
Now i have one ContextProvider with all of them inside:
<AllContext.Provider
value={{
setProfile, // second function that changes profile object using useState to false or updated value
profileReload, // function that triggers fetch profile object from server
deviceTheme, // object
setDeviceTheme, // second function that changes theme object using useState to false or updated value
clickEvent, // click event
usePopup, // second function of useState that trigers some popup
popup, // Just pass this to usePopup component
windowSize, // manyUpdates on resize (like 30 a sec, but maybe can debounce)
windowScroll // manyUpdates on resize (like 30 a sec, but maybe can debounce)
}}
>
But like sad in docs:
Because context uses reference identity to determine when to re-render, there are some gotchas that could trigger unintentional renders in consumers when a provider’s parent re-renders. For example, the code below will re-render all consumers every time the Provider re-renders because a new object is always created for value:
This is bad:
<Provider value={{something: 'something'}}>
This is ok:
this.state = {
value: {something: 'something'},
};
<Provider value={this.state.value}>
I imagine that in future i will have maybe up to 30 context providers and it's not very friendly :/
So how can i pass this global values and functions to components? I can just
Create separate contextProvider for everything.
Group something that used together like profile and it's functions,
theme and it's functions (what about reference identity than?)
Maybe group only functions because thay dont change itself? what
about reference identity than?)
Other simpliest way?
Examples of what i use in Provider:
// Resize
const [windowSize, windowSizeSet] = useState({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight
})
// profileReload
const profileReload = async () => {
let profileData = await fetch('/profile')
profileData = await profileData.json()
if (profileData.error)
return usePopup({ type: 'error', message: profileData.error })
if (localStorage.getItem('deviceTheme')) {
setDeviceTheme(JSON.parse(localStorage.getItem('deviceTheme')))
} else if (profileData.theme) {
setDeviceTheme(JSON.parse(JSON.stringify(profileData.theme)))
} else {
setDeviceTheme(settings.defaultTheme)
}
setProfile(profileData)
}
// Click event for menu close if clicked outside somewhere and other
const [clickEvent, setClickEvent] = useState(false)
const handleClick = event => {
setClickEvent(event)
}
// Or in some component user can change theme just like that
setDeviceTheme({color: red})
The main consideration (from a performance standpoint) for what to group together is less about which ones are used together and more about which ones change together. For things that are mostly set into context once (or at least very infrequently), you can probably keep them all together without any issue. But if there are some things mixed in that change much more frequently, it may be worth separating them out.
For instance, I would expect deviceTheme to be fairly static for a given user and probably used by a large number of components. I would guess that popup might be managing something about whether you currently have a popup window open, so it probably changes with every action related to opening/closing popups. If popup and deviceTheme are bundled in the same context, then every time popup changes it will cause all the components dependent on deviceTheme to also re-render. So I would probably have a separate PopupContext. windowSize and windowScroll would likely have similar issues. What exact approach to use gets deeper into opinion-land, but you could have an AppContext for the infrequently changing pieces and then more specific contexts for things that change more often.
The following CodeSandbox provides a demonstration of the interaction between useState and useContext with context divided a few different ways and some buttons to update the state that is held in context.
You can go to this URL to view the result in a full browser window. I encourage you to first get a handle for how the result works and then look at the code and experiment with it if there are other scenarios you want to understand.
This answer already does a good job at explaining how the context can be structured to be more efficient. But the final goal is to make context consumers be updated only when needed. It depends on specific case whether it's preferable to have single or multiple contexts.
At this point the problem is common for most global state React implementations, e.g. Redux. And a common solution is to make consumer components update only when needed with React.PureComponent, React.memo or shouldComponentUpdate hook:
const SomeComponent = memo(({ theme }) => <div>{theme}</div>);
...
<AllContext>
{({ deviceTheme }) => <SomeComponent theme={deviceTheme}/>
</AllContext>
SomeComponent will be re-rendered only on deviceTheme updates, even if the context or parent component is updated. This may or may not be desirable.
The answer by Ryan is fantastic and you should consider that while designing how to structure the context provider hierarchy.
I've come up with a solution which you can use to update multiple values in provider with having many useStates
Example :
const TestingContext = createContext()
const TestingComponent = () => {
const {data, setData} = useContext(TestingContext)
const {value1} = data
return (
<div>
{value1} is here
<button onClick={() => setData('value1', 'newline value')}>
Change value 1
</button>
</div>
)
}
const App = () => {
const values = {
value1: 'testing1',
value2: 'testing1',
value3: 'testing1',
value4: 'testing1',
value5: 'testing1',
}
const [data, setData] = useState(values)
const changeValues = (property, value) => {
setData({
...data,
[property]: value
})
}
return (
<TestingContext.Provider value={{data, setData: changeValues}}>
<TestingComponent/>
{/* more components here which want to have access to these values and want to change them*/}
</TestingContext.Provider>
)
}
You can still combine them! If you are concerned about performance, you can create the object earlier. I don't know if the values you use change, if they do not it is quite easy:
state = {
allContextValue: {
setProfile,
profileReload,
deviceTheme,
setDeviceTheme,
clickEvent,
usePopup,
popup,
windowSize
}
}
render() {
return <AllContext.Provider value={this.state.allContextValue}>...</AllContext>;
}
Whenever you then want to update any of the values you need to do I like this, though:
this.setState({
allContextValue: {
...this.state.allContextValue,
usePopup: true,
},
});
This will be both performant, and relatively easy as well :)
Splitting those up might speed up a little bit, but I would only do that as soon as you find it is actually slow, and only for parts of your context that would have a lot of consumers.
Still, if your value does not change a lot, there is really nothing to worry about.
Based on Koushik's answer I made my own typescipt version.
import React from "react"
type TestingContextType = {
value1?: string,
value2?: string,
value3?: string,
value4?: string,
value5?: string,
}
const contextDefaultValues = {
data: {
value1: 'testing1',
value2: 'testing1',
value3: 'testing1',
value4: 'testing1',
value5: 'testing1'
} as TestingContextType,
setData: (state: TestingContextType) => {}
};
const TestingContext = React.createContext(contextDefaultValues);
const TestingComponent = () => {
const {data, setData} = React.useContext(TestingContext);
const {value1} = data
return (
<div>
{value1} is here
<button onClick={() => setData({ value1 : 'newline value' })}>
Change value 1
</button>
</div>
)
}
const App = () => {
const [data, setData] = React.useState(contextDefaultValues.data)
const changeValues = (value : TestingContextType) => setData(data && value);
return (
<TestingContext.Provider value={{data, setData: changeValues}}>
<TestingComponent/>
{/* more components here which want to have access to these values and want to change them*/}
</TestingContext.Provider>
)
}