Use GraphQL data in gatsby-browser? - javascript

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.

Related

Gatsby fetch data from local storage best practice

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.

Can i use the document object in React?

For an example if we made a App component and we needed to create an element each time a button was clicked:
function App() {
const handleClick = () => {
// Code here
}
return (
<div id="app">
<button onClick={handleClick}>Click here</button>
</div>
)
}
Is it ok if i used document.createElement("div") and document.getElementById("app").append() in that case?
function App() {
const handleClick = () => {
let div = document.createElement("div")
div.innerHTML = "Hi!"
document.getElementById("app").append(div)
}
return (
<div id="app">
<button onClick={handleClick}>Click here</button>
</div>
)
}
It's fine to use the document object for certain things in React code, but not in this case. That's not how you'd add an element to your #app div. Instead, you'd use state for it. When switching to an MVC/MVVM/whatever tool like React, you need to stop thinking in terms of modifying the DOM and start thinking in terms of component states.
In your case, for instance, you'd either want a boolean state member telling you whether to render that Hi! div, or perhaps an array state member of messages you might display.
Here's an example of the former:
const { useState } = React;
const App = () => {
// The state information
const [showHi, setShowHi] = useState(false);
const handleClick = () => {
// Set the state to true on button click
setShowHi(true);
};
return <div>
{/* Check the state and conditionally render */}
{showHi && <div>Hi!</div>}
<button onClick={handleClick}>Click here</button>
</div>;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
I suggest working through the tutorial on the React website for a solid introduction to React.
Can you?
You can, but this goes against the idea of React in the first place and is advised against. Updating the DOM this way can cost you performance and even introduce bugs in your code.
The point of React is to handle updating the DOM in a performant way that is cross-browser compatible. In fact, behind the scenes, React is going to create the <div> element and place it in the DOM, but it is going to do so in a less costly and better-managed way by using a virtual representation of the DOM and everything in it. This is not as expensive as directly building, destroying and rebuilding elements on the page, because, for one, not everything on the page needs to be changed each time a user interaction that changes something of the page happens. React will react to parts of the page that have changed and keep parts that have not changed improving the performance on your web app. (See
Reactive Updates), This is one of the reasons the React library was built.
React keeps its own internal registry of elements it renders on the page, in the virtual DOM. It updates and tracks them all through their lifecycle on the page. Because of this, it knows which elements to replace, keep or tear down during user interaction. So creating an element by using the document object directly circumvents the creation (and registration) of a representation of the element in the virtual DOM making React unaware of the element - leaving the lifecycle handling of the element to the browser.
The React way
Because of the above, anything that has to do with the UI; including rendering, updating and destroying is best left to React.
The way to build (thanks to JSX and this is an improvement to #yanir's answer) your element is by simply writing out the element where you need it (or storing it in a variable first and using embedded JSX). The innerHTML attribute can be an embedded expression that is computed in the div element. Don't worry, this operation won't cost as much as using the document object to create elements directly. We'll just need a form of state to track how many times the user has clicked and create a plain JavaScript object then use the map() method to create as many <div>s as needed.
function App() {
const [items, setItems] = useState([]);
const handleClick = () => {
let obj = { value: "Hi!" };
setItems([...items, obj]);
};
return (
<div id="app">
<button onClick={handleClick}>Click here</button>
{items.map((item, index) => {
return <div key={index}>{item.value}</div>;
})}
</div>
)
}
If you need to interact with the DOM directly, through the document object for example, as #Kaushik suggested, you can track the element through the use of hooks like useRef(), to get a reference to the object, useId() for unique stable Ids across server and client, useLayoutEffect() to hook into the browser after DOM updates but before it paints and so on. That being said, there are some APIs and attributes that you may need to access from the document object like events, title, URL, and several other attributes. In as much, as these do not mutate the UI, you can use these when needed, while the UI operations are left to React to handle.
Yes, you can. But the React way to do this is to keep track of this in state. You can keep count of the number of divs you want to render, and update that. State update will trigger a rerender, and your view will be updated.
Below I am using a state variable count and initializing an Array using the Array() constructor of size equal to count.
function App() {
const [count,setCount] = 0;
const handleClick = () => {
setCount(count => count+1);
}
return (
<div id="app">
<button onClick={handleClick}>Click here</button>
{Array(count).map((x) => <div>{/**YOUR DIV CONTENT **/}<div>)}
</div>
)
}
If you need to access the DOM element, the recommended approach is to use the useRef, useImperativeHandle, useLayoutEffect, or useid hooks, as these allow React to be aware of when you are using the DOM elements, and allows for future React updates to not break your existing behavior. In your example though, I would argue that you do not need access to the DOM element, and that you should instead can let React handle the rendering of the element via declarative JSX.
You don't you document.getElementById in reactjs
All point of react is to use jsx (dynamic HTML)
What you can to it's to create an array that you append item to this array each click and use map to render the new item each time :
function App() {
const [items,setItems] = useState([])
const handleClick = () => {
// Code here
setItems(prev => {
let arr = []
//change obj each time to your need.
let obj = {label : "test" , value : "test"}
arr.push(obj)
setItems(arr)
})
}
return (
<div id="app">
<button onClick={handleClick}>Click here</button>
{items.map((item,index) =>{
return <p key={index}>{item.label}</p>
})
</div>
)
}

React Component with different parts in different locations

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>
)
}

react gatsby: adding css class dynamically not working in prod build

I need to add a css class dynamically on a div. For this I've set the css class like this:-
const Container = ({ divClass }) => {
const [cssClass, setcssClass] = useState(divClass)
return (
<div id="containerDiv" className={cssClass} ></div>
{/* <div id="containerDiv" className={divClass} ></div> even this doesn't work*/}
)
}
This works as expected in development build, but doesn't work in prod build. Even without using state if I set class to divClass it's not working in prod build.
It only works after I set state using setcssClass, like this-
const Container = ({ divClass }) => {
const [cssClass, setcssClass] = useState(divClass)
return (
<div id="containerDiv" className={cssClass} onClick={() => setcssClass('testtt')}></div>
)
}
Can anypone please explain this discrepancy in development and production build?
Your code is not working on your first try because at the moment you are defining const [cssClass, setcssClass] = useState(divClass), your <div> and it's class (divClass or cssClass) is not defined yet.
The solution is to use a useEffect hook with empty deps ([]) since it's a function that is triggered once the DOM tree is loaded (and your <div> in it).
I wouldn't recommend the approach of the other answer (besides being effective) because it impacts directly the DOM and that's the reason why you are using React and not jQuery (you creating a virtual DOM to manipulate). You can achieve the same result by using a React-based approach with useRef hook.
const Container = ({ divClass }) => {
const containerDivRef = useRef(null);
useEffect(() => {
containerDivRef.current.classList.add(divClass)
}, []);
return (
<div ref={containerDivRef}></div>
)
}
Your <div> information is stored inside the containerDivRef.current exactly in the same way that using document.querySelector but without affecting the DOM.
The discrepancy that you mention between develop and build is caused by the webpack's assets treatment and Gatsby's configuration. Since with a gatsby develop (runtime) allows you to use browser's APIs (such as window, document, other global or DOM objects) in the gatsby build (buildtime) the code is processed in the server-side, where those variables are not allowed (even created), that's why you usually should wait for them. You can check for further details in the Overview of the Gatsby Build Process documentation.
const Container = ({ divClass }) => {
useEffect(() => {
const containerDiv = document.querySelector("#containerDiv");
containerDiv.classList.add(divClass);
}, []);
return (
<div id="containerDiv"></div>
)
}
Have you tried manipulating the dom manually inside useEffect?
I know this might not be the most ideal solution but maybe it solves the problem. lol

React DnD: Avoid using findDOMNode

I don't fully understand it but apparently it isn't recommended to use findDOMNode().
I'm trying to create drag and drop component but I'm not sure how I should access refs from the component variable. This is an example of what I currently have:
const cardTarget = {
hover(props, monitor, component) {
...
// Determine rectangle on screen
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
...
}
}
Source
Edit
It might be caused by my component being both the drag and drop source and target as I can get it to work in this example but not this one.
Assuming you're using es6 class syntax and the most recent version of React (15, at time of writing), you can attach a callback ref like Dan did in his example on the link you shared. From the docs:
When the ref attribute is used on an HTML element, the ref callback receives the underlying DOM element as its argument. For example, this code uses the ref callback to store a reference to a DOM node:
<h3
className="widget"
onMouseOver={ this.handleHover.bind( this ) }
ref={node => this.node = node}
>
Then you can access the node just like we used to do with our old friends findDOMNode() or getDOMNode():
handleHover() {
const rect = this.node.getBoundingClientRect(); // Your DOM node
this.setState({ rect });
}
In action:
https://jsfiddle.net/ftub8ro6/
Edit:
Because React DND does a bit of magic behind the scenes, we have to use their API to get at the decorated component. They provide getDecoratedComponentInstance() so you can get at the underlying component. Once you use that, you can get the component.node as expected:
hover(props, monitor, component) {
const dragIndex = monitor.getItem().index;
const hoverIndex = props.index;
const rawComponent = component.getDecoratedComponentInstance();
console.log( rawComponent.node.getBoundingClientRect() );
...
Here it is in action:
https://jsfiddle.net/h4w4btz9/2/
Better Solution
A better solution is to just wrap your draggable component with a div, define a ref on that and pass it to the draggable component, i.e.
<div key={key} ref={node => { this.node = node; }}>
<MyComponent
node={this.node}
/>
</div>
and MyComponent is wrapped in DragSource. Now you can just use
hover(props, monitor, component) {
...
props.node && props.node.getBoundingClientRect();
...
}
(props.node && is just added to avoid to call getBoundingClientRect on an undefined object)
Alternative for findDOMNode
If you don't want to add a wrapping div, you could do the following.
The reply of #imjared and the suggested solution here don't work (at least in react-dnd#2.3.0 and react#15.3.1).
The only working alternative for findDOMNode(component).getBoundingClientRect(); which does not use findDOMNode is:
hover(props, monitor, component) {
...
component.decoratedComponentInstance._reactInternalInstance._renderedComponent._hostNode.getBoundingClientRect();
...
}
which is not very beautiful and dangerous because react could change this internal path in future versions!
Other (weaker) Alternative
Use monitor.getDifferenceFromInitialOffset(); which will not give you precise values, but is perhaps good enough in case you have a small dragSource. Then the returned value is pretty predictable with a small error margin depending on the size of your dragSource.
React-DnD's API is super flexible—we can (ab)use this.
For example, React-DnD lets us determine what connectors are passed to the underlying component. Which means we can wrap them, too. :)
For example, let's override the target connector to store the node on the monitor. We will use a Symbol so we do not leak this little hack to the outside world.
const NODE = Symbol('Node')
function targetCollector(connect, monitor) {
const connectDropTarget = connect.dropTarget()
return {
// Consumer does not have to know what we're doing ;)
connectDropTarget: node => {
monitor[NODE] = node
connectDropTarget(node)
}
}
}
Now in your hover method, you can use
const node = monitor[NODE]
const hoverBoundingRect = node.getBoundingClientRect()
This approach piggybacks on React-DnD's flow and shields the outside world by using a Symbol.
Whether you're using this approach or the class-based this.node = node ref approach, you're relying on the underlying React node. I prefer this one because the consumer does not have to remember to manually use a ref other than the ones already required by React-DnD, and the consumer does not have to be a class component either.

Categories

Resources