Current I have a React component which have 2 parts, they are tightly coupled together.
The problem is I need these 2 parts display in different location for different pages.
Please see the below wireframe for illustration.
My approach currently is splitting the component's part into 2 separated components and there are event handling code between them which i need to copy and paste to every page that contains these parts.
Is there a better way to structure these parts as a single component to void code duplication of even handling and allow flexibility to place them in any location we want within a page.
Thanks.
If it works to have a single copy of the control code (eg, if you just want a single state that's shared between all pages, with each page being able to alter the behavior of other pages), you could put the logic in a component near the top of the component tree, where it's a common ancestor to all pages. It can then use context to make any values available to the pages.
const ExampleContext = React.createContext();
const ExampleProvider = () => {
// Put all your event handling code that you've been copy/pasting here.
// Any data that needs to be available to part1 and part 2, put here.
// Memoization is needed if you're creating an object or array; you can
// skip it if you only need to provide a single primitive.
const value = useMemo(() => {
return {
someValue,
someFunction,
etc
}
}, [/* dependencies */]);
return (
<ExampleContext.Provider value={value}>
{/* these pages can be farther down the tree; they just need to be descendants of ExampleContext.Provider */}
<Page1/>
<Page2/>
</ExampleContext.Provider>
)
}
// Then consume the context like this:
const Page1 = () => {
const value = useContext(ExampleContext);
return (
<div>
<Part1 /* whatever props are needed from value *//>
<Component1/>
<Part2 /* whatever props are needed from value *//>
</div>
)
}
If each page needs to be independant of the others (have their own state and event listeners), then instead of copy pasting the code, you can extract the logic into a custom hook for easy reuse:
const useExample = () => {
// put all your event handling code that you've been copy/pasting here
// return any values that will be needed by Part1 and Part2
return {
someValue,
someFunction,
etc
}
}
// Used like:
const Page1 = () => {
const value = useExample();
return (
<div>
<Part1 /* whatever props are needed from value *//>
<Component1/>
<Part2 /* whatever props are needed from value *//>
</div>
)
}
Related
I would like to fetch data from local storge (in runtime) every time the app starts, then I store it in a store.
Gatsby docs explain the way to do this:
https://www.gatsbyjs.com/docs/conceptual/data-fetching/
Basically just use the useEffect hook in a page and get the data from local storage. However, I would like to get this data independently of the page being visited. For example, if I fetch the data on the index page, and the user refreshes another page, the data won't be fetched. I would like to do it in the equivalent of App.tsx file in a regular React app.
My current solution is to do it in wrap-pages file:
const MyLocalStorage = ({ children }) => {
const { getLocalStorage} = fromStore()
useEffect(() => {
getLocalStorage() // fetches data from local storage
}, [])
return null
}
export function wrapPagesDeep({ element }) {
return (
<>
<MyLocalStorage/>
{element}
</>
)
}
This however doesn't make much sense. This file is intended for wrapping components, not for data fetching. What would be the correct way to do that please?
There are multiple ways depending on your architecture, design system, and use cases (for example, from a provider to a wrapper, from an isolated service to a controller, etc.).
There is no such thing as "best practice" without knowing everything involved in the decision: making some super complicated and isolated logic (like adding an MVVM: controller, stores, etc.) may look good but can be an extremely bad practice for a simple scenario, and vice-versa: an easy and straightforward approach can be a bad solution for a complex app.
Following your approach, I think it could be easily isolated (and reused) by moving this logic into a Layout (or a wrapper that wraps your application) and adding a location prop to it. Something like:
const Layout = ({ children, location = {} }) => {
const { getLocalStorage} = fromStore()
useEffect(() => {
getLocalStorage() // fetches data from local storage
}, [location])
return (
<>
<main>{children}</main>
</>
)
}
export default Layout
Then, in every use of Layout:
const SomePage = ({ location }) => {
return (
<Layout location={location}>
<h1>Some content</h1>
</Layout>
);
};
Note: location prop is inherited by default in all top-level components (pages and templates) as you can see in the docs
So every time the location changes, you will fetch the local storage data. This can be easily moved to a provider that updates the value automatically. You will only need to wrap your application accordingly.
In a nested hook: how could one know if it already was invoked in the current component (instance) and access any previously computed/saved values?
Preferably without the Component author/the hook user having to know about this and not having to do anything special for it to work.
Example
An example to illustrate the problem:
const useNestedHook = () => {
// Some heavy work with the same result for each component instance.
// Is it possible to remember the result of this work when
// this hook is used again in the same component instance?
// So I would like to save a state which all uses of useNestedHook
// could access as long as they are in the same component instance.
}
const useHookA = () => {
useNestedHook();
};
const useHookB = () => {
useNestedHook();
};
const Component = () => {
useHookA();
// Would like to avoid useNestedHook repeating its work since it's
// used in the same component and would have this same result (per definition)
// Preferably without the Component author having to know anything about this.
useHookB();
};
Imagined solution
Something like a "named" shared state, which would give access to the same shared state (in the same component instance) no matter which hook it's used in. With each component instance having its own separate state as usual. Maybe something like:
const [state, setState] = useSharedState("stateId", initialValue);
No, that is not possible. Each useState() call will always be separate from other useState() calls.
The component can not use the hooks as in your example, but the component author doesn't necessarily have to care for the implementation details.
A solution would depend on the use case.
Some details:
One state is defined by where the useState() call is written in the code (see explanation), which is not directly related to the instance. I.e. two useState() calls and two instances are 4 state values.
You can use shared state e.g. using context, but then the state would also be shared by all instances, not only the hooks (which you don't want).
So the useNestedHook() will always be "separate", but if you can use shared state, and you only care for "caching", and can accept that the useNestedHook() is called twice (i.e. skip the expensive operation if the result is the same), then you can use useEffect(). I.e. the call would depend on the value, not the instance and not the hook.
Some examples:
1. One hook with options
E.g. if your hooks A and B would optionally calculate two different values, which need the same useNestedHook() value, you could create one hook with options instead, e.g.:
const useHookAB = ({ A, B }) => {
const expensiveValue = useNestedHook();
if( A ){ /* do what useHookA() was doing */ }
if( B ){ /* do what useHookB() was doing */ }
};
const Component = () => {
useHookAB({ A: true, B: true });
};
I can't imagine another reason right now why you would want to call the hooks like that.
2. The "normal" way
The obvious solution would be:
const useHookA = ( value ) => {
// ...
};
const useHookB = ( value ) => {
// ...
};
const Component = () => {
const value = useNestedHook();
useHookA( value );
useHookB( value );
};
But I can imagine reasons why you can't (or don't like to) do it that way.
I intend to move out logic from my app.js. Right now, in app.js, I have a function that changes state of some variables in App.js. I want to learn what is the correct way to change the state inside app.js by letting app.js call a function (for example on click) that is in another component.
my app.js looks like the following:
const App = () => {
const [opacity, opacitySet] = React.useState(1);
const [showStep, setDisplayStep] = React.useState(true);
const [isQrActive, setQrState] = React.useState(false);
/** code **/
<ButtonContinue
onClick={() => {
stateChanger();
}}
style={{ backgroundColor: "black" }}
>
<p>cancel</p>
</ButtonContinue>
/** lots of code **/
function stateChanger() {
opacitySet(0);
setTimeout(() => {
opacitySet(1);
setDisplayStep(!showStep);
setQrState(!isQrActive);
}, 400);
}
/** and more code **/
}
I want to move the stateChanger function out from app.js, but still keep the same functionality. This in turn would allow me to call stateChanger from other classes easily.
The simplest solution (which is also the most difficult to scale) is to pass the function as a prop to its children. You could also create a custom hook. But by declaring the state on the root-level component App and asking about how you can generally modify it from another component, we can interpret that the question is essentially about how to handle global state. You may want to consider using the Context API or Redux, which are tools created specifically for that purpose.
The most important thing to remember here is that if we have the option of maintaining the state reasonably close to where it is being used and modified, we should do that instead of handling it as part of a global state.
I have an app with some route ID's (basically a bunch of sections in a long SPA) that I have defined manually. I fetch these in gatsby-browser.js and use them in conjunction with shouldUpdateScroll, checking if the route ID exist, and in that case, scroll to the position of the route/section.
Example:
export const shouldUpdateScroll = ({ routerProps: { location } }) => {
const container = document.querySelector('.site')
const { pathname } = location
const projectRoutes = [`project1`, `project2`]
if (projectRoutes.indexOf(pathname) !== -1) {
const target = document.getElementById(pathname)
container.scrollTop = target.offsetTop;
}
return false
}
This works well for my usecase.
Now I want to add something similar for a page where the content is dynamically created (fetched from Sanity). From what I understand I cannot use GraphQL in gatsby-browser.js, so what is the best way to get the ID's from Sanity to gatsby-browser.js so I can use them to identify their scroll positions?
If there's some other better way to achieve the same result I'm open to that of course.
I think that you are over complexing the issue. You don't need the gatsby-browser.js to achieve it.
First of all, because you are accessing directly to the DOM objects (using document.getElementById) and you are creating precisely a virtual DOM with React to avoid pointing the real DOM. Attacking directly the real DOM (like jQuery does) has a huge performance impact in your applications and may cause some issues since in the SSR (Server-Side Rendering) the element may not be created yet.
You are hardcoding a logic part (the ids) on a file that is not intended to do so.
I think you can achieve exactly the same result using a simple function using a few hooks.
You can get the same information as document.getElementById using useRef hook and scrolling to that position once needed.
const YourComponent= (props) => {
const sectionOne = useRef(null);
const sectionTwo = useRef(null);
useEffect(()=>{
if(typeof window !== `undefined`){
console.log("sectionOne data ",sectionOne.current)
console.log("sectionTwo data ",sectionTwo.current)
if(sectionOne) window.scrollTo( 0, 1000 ); // insert logic and coordinates
}
}, [])
return (
<>
<section ref={sectionOne}>Section 1</section>
<section ref={sectionTwo}>Section 2</section>
</>
);
}
You can isolate that function into a separate file in order to receive some parameters and return some others to achieve what you want. Basically, the snippet above creates a reference for each section and, once the DOM tree is loaded (useEffect with empty deps, []) do some stuff based on your logic.
Your document.getElementById is replaced for sectionOne.current (note the .current), initially set as null to avoid unmounting or cache issues when re-hidration occurs.
I'm having an array data.info that is being updated over time and I'm trying to replace placeholder rendered elements with another. So by default app.js looks like this
return (
<Fragment>
{data.info.map((index) => {
return <Typography key={index} variant="h6" className={classes.title}>Demo</Typography>
})}
</Fragment>
)
Also I have a hook with async function to subscribed to data.info.length.
useEffect(
() => {
if (!initialRender.current) {
if (data.info.length!==0) {
for (let i = data.info.length-iScrollAmount+1 ; i < data.info.length+1; i++) {
firstAsync(i)
}
}
} else {
initialRender.current = false
}
},
[data.info.length]
)
async function firstAsync(id) {
let promise = new Promise(() => {
setTimeout(() => console.log(document.getElementById(id)), 500)
});
}
With document.getElementById() and id I can get to every element that was rendered and change it. And here goes the problems.
I'm using material-ui so I can't get to <Typography/> because it is transformed into <h6/>. Probably that is not a problem since I need to replace contents, so I can find parent element and remove all children. Is that way correct?
After I delete children how do I add content using jsx? What I mean is that in async function I'll get an array that I want to use in new element <NewCard/> to dynamically put into <Fragment/>. Yet I did not find any example how to do that.
It is not a good practice to change DOM Nodes directly in React, and you need to let React do the rendering for you and you just tell react what to do.
in your case you need to define a React State for your data and set your state inside your firstAsync function and then use your state to render whatever html element or React component which you want
React does not encourage the practice of manipulating the HTML DOM nodes directly.
Basically you need to see 2 things.
State which is a special variable whose value is retained on subsequent refresh. Change in reference in this variable will trigger component and its children a refresh/re-render.
Props which is passed to every Component and is read only. Changing in props causes refresh of component by default.
In your example, based on data.info you want to render Typography component.
Solution
First thing is your map function is incorrect. First parameter of map function is item of list and second is index. If you are not sure if info will always be present in data, you may want to have a null check as well.
{(data.info || []).map((info, index) => {
return <Typography key={index} variant="h6" className={classes.title}>{info.text}</Typography>
})}
You should be passing info from map to Typography component. Or use info value in content of Typography as shown above.
Update data.info and Typography will update automatically. For this, please make sure, data.info is a component state and not a plain variable. Something like
const [data, setData] = React.useState({});
And when you have value of data (assuming from API), then
setData(responseApi);