Preact isn't replacing dom elements on rehydration? - javascript

I'm trying to learn rehydration of dom using preact. For some unknown reason, the render function isn't replacing the original DOM node but rather appending to it.
https://github.com/preactjs/preact/issues/24, The 3rd parameter of render should afford the opportunity to replace:
render(<App />, into, into.lastChild);
https://codesandbox.io/s/beautiful-leavitt-rkwlw?file=/index.html:0-1842
Question: Any ideas on how I can ensure that hydration works as one would expect it to e.g. replace the static counter with the interactive one?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Test</title>
</head>
<body>
<script>
window.__STATE__ = { components: {} };
</script>
<main>
<div>
<script data-cmp-id="1">
window.__STATE__.components[1] = {
name: "Counter",
props: { id: 1 }
};
</script>
<div>HOW MANY LIKES 0</div>
<button>Increment</button>
</div>
</main>
<script type="module">
import {
html,
useState,
render
} from "https://unpkg.com/htm/preact/standalone.module.js";
let id = 0;
export const withHydration = Component => props => {
id += 1;
return html`
<${Component} ...${props} />
`;
};
const Counter = () => {
const [likes, setLikes] = useState(0);
const handleClick = e => {
e.preventDefault();
setLikes(likes + 1);
};
return html`
<div>HOW MANY LIKES ${likes}</div>
<button onClick=${handleClick}>Increment</button>
`;
};
const componentMap = {
Counter: withHydration(Counter)
};
const $componentMarkers = document.querySelectorAll(`[data-cmp-id]`);
Array.from($componentMarkers).forEach($marker => {
debugger;
const $component = $marker.nextElementSibling;
const { name, props } = window.__STATE__.components[
$marker.dataset.cmpId
];
const Component = componentMap[name];
render(
html`
<${Component} ...${props} />
`,
$component.parentNode,
$component
);
});
</script>
</body>
</html>
All of this is inspired by https://github.com/maoberlehner/eleventy-preact repo.

There are two things going on here, I'll explain each:
1. The third argument to render() should not be needed here.
Your Counter component has two elements at the root (<div> and <button>), and passing a single DOM element reference as the third argument to render is going to prevent Preact from using the <button> that exists in the "prerendered" DOM.
By default, render(vdom, parent) will look at all of the children of parent and figure out which ones should be re-used when attaching to existing DOM. There is only one very specific case where this behavior doesn't work and the third argument is warranted, which is when multiple "render roots" share the same parentNode. In general that's a case best avoided, which is why that third parameter isn't really advertised much in documentation.
2. `htm/preact/standalone` currently appears to be broken
I'd come across a similar issue last week, so I knew to check this. For some reason, when we bundled Preact into HTM to create the standalone build, it broke rendering. It's likely a result of overly aggressive minification, and should be fixed soon.
In the meantime, it's possible (and sometimes better) to use htm + preact + preact/hooks directly from unpkg. The key is to use the fully resolved module URLs, so that unpkg's ?module parameter is converting imports to the same URLs you've used for your manual ones. Here's the correct URLs for your demo:
import htm from "https://unpkg.com/htm#latest?module";
import { h, render } from "https://unpkg.com/preact#latest?module";
import { useState } from "https://unpkg.com/preact#latest/hooks/dist/hooks.module.js?module";
With the third render argument removed and those imports swapped out, your demo actually works fine: đź‘Ť
https://codesandbox.io/s/fast-fire-dyzhg?file=/index.html:719-954
Bonus Round: Hydration
My head is very much in the hydration space right now, so this is of great interest to me. There's a couple of things I would recommend changing in your approach based on my research:
1. Use JSON for data instead of script tags
Inline scripts block rendering and force all stylesheets to be loaded fully prior to executing. This makes them disproportionately expensive and worth avoiding at all costs. Thankfully, the solution is pretty simple: instead of using <script>__STATE__[1]={..}</script> for your component hydration data/callsites, switch to <script type=".."> with a non-JavaScript mimetype. That will make the script non-blocking, and you can quickly and easily parse the data as JSON when you hydrate - far faster than evaluating JS, and you control when it happens. Here's what that looks like:
<div data-component="Counter">
<div>HOW MANY LIKES 0</div>
<button>Increment</button>
<script type="text/hydration-data">
{"props":{"id":1}}
</script>
</div>
Notice that you can now use the location of that script tag inside your data-component root to associate it with the component without the need for global IDs.
Here's a fork of the fixed version of your demo, with the above change:
https://codesandbox.io/s/quirky-wildflower-29944?file=/index.html:202-409
I hope you find the updated root/data/render loop to be an improvement.
2. Use hydrate() to skip diffing
If you know that your pre-rendered HTML structure exactly matches the initial DOM tree structure your components are going to "boot up" into, hydrate() lets you bypass all diffing, boot up quickly and without touching the DOM. Here's the updated demo with render() swapped out for hydrate() - no functional difference, just it'll have better performance:
https://codesandbox.io/s/thirsty-black-2uci3?file=/index.html:1692-1709

Related

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

Dynamically load and unload content of css file in javascript/react

I've been banging my head against this for a few days now. I read all the similar questions, but they don't quite match what I need.
I find it hard to believe no one else has tried, or documented, this before, and I'd love some thoughts on how to approach this.
Requirement
I am using react bootstrap for my app and I want to give users the choice between a set of bootstrap themes. To keep it simple I currently only have a light and a dark one. These styles ship as scss from https://bootswatch.com/ and so I'd rather not try the usual CSS in JS theme approaches.
What I'm trying to do
Ideally, what I want is the ability to conditionally render/import the css files. I've tried a few things most of them don't work and one works sort of, but is really hacky.
What I'd like is a way to construct a DOM node with a style node from the css file and not render it. Then I can put all these style nodes into the DOM and selectively add/remove them.
Ideally, I don't need to manually alter the DOM at all. I haven't been able to find a way to do it. I'd have hoped conditional rendering of my style component would do the trick, but this doesn't work as the style is never loaded (beyond first time) or unloaded (ever).
What I tried so far
Use lazy loading (works sort of)
I put my css in a lightweight component such as
import React from 'react';
import './_light.scss';
const Theme = React.forwardRef(( props, ref) => (<div ref={ref}></div>));
export default Theme;```
Then I lazy load either or in my App like so
const LightTheme = React.lazy(() => import('../themes/lightTheme'));
const DarkTheme = React.lazy(() => import('../themes/darkTheme'));
....
const element = (chosenTheme === PreferredTheme.LIGHT) ?
<LightTheme /> : <DarkTheme />
...
return (
<ThemeContext.Provider value={initialValue}>
<React.Suspense fallback="{<></>}">
{element}
</React.Suspense>
{children}
</ThemeContext.Provider>
)
The problem is that I give the user the choice to toggle themes and what seems to happen is two problems 1) that once both have loaded (once) they never load again 2) ordering of style nodes matters and so whichever was loaded last will always stay active.
I was able to work around this by doing some hacky <head> node manipulation in my useEffect to delete (no longer used style)/add (to be applied) style nodes to the DOM, but it's hacky at best.
useRef
to get a reference to each style node, save it to a dict and then delete it. This breaks in an interesting way where the reference works the first time the thing renders but on subsequent renders the reference becomes undefined
Creating a stylesheet on the fly
The solution here looks promising How to load/unload css dynamically. Trouble is I don't know how to get to the inner text of my stylesheet in this scenario. As I said it's shipped as scss so it gets preprocessed and I don't know how I could even get the text.
Any thought or explanations much appreciated.
Right, so I found a quite neat solution now that at least has straightforward logic. I had to learn a bit more about Webpack and how its loaders work. Specifically how to use CRA and still be able to modify it's behaviour. Long story short is that CRA webpack chains the style-loader at the end of scss files and therefore tries to put the css into the DOM as a node (https://webpack.js.org/loaders/style-loader/)
Since I don't want to do that (yet) I need to disable the style-loader that is taken care of by this abomination of an import statement (https://webpack.js.org/concepts/loaders/#inline)
// #ts-ignore
import { default as DarkThemeC } from '!css-loader!sass-loader!../themes/_dark.scss'; // eslint-disable-line import/no-webpack-loader-syntax
// #ts-ignore
import { default as LightThemeC } from '!css-loader!sass-loader!../themes/_light.scss'; // eslint-disable-line import/no-webpack-loader-syntax
Now I have the css as an array at my disposal without rendering it so I put it into a Map for future use.
const availableThemes = new Map<PreferredTheme, string>();
availableThemes.set(PreferredTheme.LIGHT, LightThemeC.toString());
availableThemes.set(PreferredTheme.DARK, DarkThemeC.toString());
This means I can do this in my useEffect callback
useEffect( () => {
// Add new sheet
const newTheme = availableThemes.get(chosenTheme);
if(newTheme) {
// injectCss borrowed from https://stackoverflow.com/questions/60593614/how-to-load-unload-css-dynamically
injectCss(newTheme, chosenTheme);
}
// Remove all others
availableThemes.forEach( (style, theme) => {
if(theme !== chosenTheme) {
var stylesheet = document.getElementById(`dynamicstylesheet${theme}`);
if (stylesheet) {
head.removeChild(stylesheet);
}
}
});
}, [chosenTheme])
Works like a charm, but the only thing I observed is that it's a bit slow on switch (fraction of a second)
Here is my solution (looks like #sev solution)
import { useEffect } from 'react';
export const useDynamicStyleSheet = (styleSheet: string): void => {
useEffect(() => {
const styleElement = document.createElement('style');
styleElement.innerHTML = styleSheet;
document.head.append(styleElement);
return () => styleElement.remove();
}, [styleSheet]);
};
import { FC } from 'react';
import { useDynamicStyleSheet } from 'app/hooks/useDynamicStyleSheet';
// eslint-disable-next-line import/no-webpack-loader-syntax, import/no-unresolved
import lightTheme from '!css-loader!antd/dist/antd.css';
const LightTheme: FC = () => {
useDynamicStyleSheet(lightTheme.toString());
return null;
};
Why not specify the theme as a class to the top-level element of your application, and build your CSS classes under that using CSS variables?
#app.dark {
--app-bg: black;
--app-fg: white;
}
#app.light {
--app-bg: white
--app-fg: black;
}
#app {
background-color: var(--app-bg);
color: var(--app-fg);
}
You could indeed use a higher-order component to wrap your UI, and change that based the selected theme, and use Webpack for the lazy loading, if it is worth it. FWIW, my experience is that dark/light themes are easiest and most clearly handled as above.

Use GraphQL data in gatsby-browser?

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.

Will JSX conditional rendering out of an object code split?

Will conditional rendering out of an object code split and lazy load as expected? Here's a short example of what I'm talking about.
const Component1 = lazy(() => import('some path'));
const Component2 = lazy(() => import('some path'));
const Component3 = lazy(() => import('some path'));
render () {
const { selectionIndex } = this.state;
<Suspense fallback={<div>Loading...</div>}>
{{
one: <Component1 />,
two: <Component2 />,
three: <Component3 />,
}[selectionIndex]}
</Suspense>
}
I want to know whether all three components will load on render, or just the one selected by selectionIndex. I'm trying to use this to conditionally select something to display based on a menu set by state, but I don't want to load everything at once.
They will not get rendered all at once. You can experiment by yourself, put console.log inside components is an easy way to find out.
React for web consists of two libs, "react" and "react-dom". "react" is in charge of encapsulating your logic intention into declarative data structures, while "react-dom" consumes these data structures and handles the actual "rendering" part of job.
The JSX element creation syntax <Component {…props} /> translates to plain JS as an API call to React.createElement(Component, props). The return value of this API call is actually just a plain object of certain shape that roughly looks like:
{
type: Component,
props: props
}
This is the aforementioned "declarative data structure". You can inspect it in console.
As you can see, calling React.createElement just return such data structure, it will not directly call the .render() method or functional component’s function body. The data structure is submitted to "react-dom" lib to be eventually "rendered".
So your example code just create those data structures, but the related component will not be rendered.
seems like its conditionally loaded based on selectionIndex. all the other components are not loaded at once.
P.S.: if you ever feel like which will get load first, just put a console log in that component and debug easily
conditionally load demo link - if you open this, the components are being loaded initially based on selectionIndex value being "one".
I'm not going to go into too much technical detail, because I feel like #hackape already provided you with a great answer as to why, point of my answer is just to explain how (to check it)
In general, I'd recommend you to download download the React Developer Tools
chrome link
firefox link
and then you can check which components are being rendered if you open the components tab inside your developer console. Here's a sandboxed example, best way to find out is to test it yourself afterall :-)
As you can see in the developer tools (bottom right), only the currently set element is being rendered

Render custom React component within HTML string from server

I have a HTML string that comes from the server, for example:
const myString = '<p>Here goes the text [[dropdown]] and it continues</p>`;
And I split this string into 3 parts so the result is the following:
const splitString = [
'<p>Here goes the text ',
'[[dropdown]]',
' and it continues</p>'
];
Then I process those 3 parts in order to replace the dropdown with a React component:
const processedArr = splitString.map((item) => {
if (/* condition that checks if it's `[[dropdown]]` */) {
return <Dropdown />;
}
return item;
}
So after all, I get the processed array, which looks like this:
['<p>Here goes the text ', <Dropdown />, ' and it continues</p>']
When I render that, it renders the HTML as a text (obviously) with the Dropdown component (that renders properly) in between the text. The problem here is that I cannot use { __html: ... } because it has to be used such as <div dangerouslySetInnerHTML={{ __html: ... }} />. I cannot add <div> around the string because that would cut out the <p> tag.
I thought about splitting the parts into tags and then in some sort of loop doing something like:
React.createElement(tagName, null, firstTextPart, reactComponent, secondTextPart);
but that would require fairly complex logic because there could be multiple [[dropdown]]s within one <p> tag and there could be nested tags as well.
So I'm stuck right now. Maybe I'm looking at the problem from a very strange angle and this could be accomplished differently in React. I know that React community discourages rendering HTML from strings, but I cannot go around this, I always have to receive the text from the server.
The only stackoverflow question I found relevant was this one, however that supposes that content coming from backend has always the same structure so it cannot be used in my case where content can be anything.
EDIT:
After some more digging, I found this question and answer which seems to be kinda solving my problem. But it still feels a bit odd to use react-dom/server package with its renderToString method to translate my component into a string and then concatenate it. But I'll give it a try and will post more info if it works and fits my needs.
So after playing with the code, I finally came to a "solution". It's not perfect, but I haven't found any other way to accomplish my task.
I don't process the splitString the way I did. The .map will look a bit different:
// Reset before `.map` and also set it up in your component's constructor.
this.dropdownComponents = [];
const processedArr = splitString.map((item) => {
if (/* condition that checks if it's `[[dropdown]]` */) {
const DROPDOWN_SELECTOR = `dropdown-${/* unique id here */}`;
this.dropdownComponents.push({
component: <Dropdown />,
selector: DROPDOWN_SELECTOR
});
return `<span id="${DROPDOWN_SELECTOR}"></span>`;
}
return item;
}).join('');
Then for componentDidMount and componentDidUpdate, call the following method:
_renderDropdowns() {
this.dropdownComponents.forEach((dropdownComponent) => {
const container = document.getElementById(dropdownComponent.selector);
ReactDOM.render(dropdownComponent.component, container);
});
}
It will make sure that what's within the span tag with a particular dropdown id will be replaced by the component. Having above method in componentDidMount and componentDidUpdate makes sure that when you pass any new props, the props will be updated. In my example I don't pass any props, but in real-world example you'd normally pass props.
So after all, I didn't have to use react-dom/server renderToString.
How about break the text apart and render the component separately?
your react component should look like this (in JSX):
<div>
<span>first text</span>
{props.children} // the react component you pass to render
<span>second part of the text</span>
</div>
and you would just call out this component with something like:
<MessageWrapper>
<DropdownComponent/> // or whatever
</MessageWrapper>

Categories

Resources