I've got code like the following where I'm calling ReactDOM.hydrate. This is shared code that sometimes gets called from the node server and sometimes in the client browser. Do I need to do anything different (then calling hydrate) when calling it on the client only. Normally, I'd call render.
const render = Component => {
ReactDOM.hydrate(
<Router history={browserHistory}>
<FullPage />
</Router>,
document.getElementById('root')
)
}
render(App);
hydrate do works similar to render on client side whether the HTML has server rendered markup or not, but when there is no markup previously like not SSR then hydrate produces some warnings but, it will render your markup as expected.
A better way to solve this would be to check if its SSR (assuming root as your parent div id) :
var isMarkupPresent = document.getElementById('root').hasChildNodes();
and then you can either render or hydrate:
isMarkupPresent ? hydrate(...) ? render(...)
Strictly speaking, no it is not safe to always use ReactDOM.hydrate().
From the docs on hydrate, you should only use it on "a container whose HTML contents were rendered by ReactDOMServer". hydrate also expects that the server rendered markup is identical to what the client side render outputs, and any differences should be considered bugs.
ReactDOM.render() on the other hand is used to render your app into an empty container on the client. You may need to do this if you don't have server rendered markup on all pages.
Because render() handles a use case that hydrate() does not, it is not safe to say "you can always use ReactDOM.hydrate()".
Related
Important note:
I am aware of createRoot and root.unmount()! Unfortunately (If I understand this correctly) they should be used just once in the application for mounting the react application.
Problem description:
In our app we have a modal component that is rendered dynamically and added to the body of the html via ReactDOM.render(). When this modal is hidden, we unmountComponentAtNode().
Unfortunately, after upgrading to react18, unmountComponentAtNode becomes deprecated and the new unmount is (in my understanding) for the root only. The same problem is about if I try to modify the ReactDOM.Render() for createRoot. Then we would have 2 roots in the app which is wrong.
What is the proper way to attach the modal to the body element (next to root!) and unmount it after it should be destroyed? The implementation is a little bit "weird" (partially in jsx, partially not...) and I would like to avoid refactoring the whole component as there will be a lot of refactoring already in the code... So I would like to focus on refactoring this component (into jsx one) later. Now I have to figure out only the rendering / unmounting. I have been thinking about using Portals, but anyway I have to create that elements somehow and render them into the DOM where portals does not help me a lot.
Calling the createRoot and then render on the root in this modal component fires an error You are calling ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before. Instead, call root.render() on the existing root instead if you want to update it. which is obvious. But there is no "useRoot()" hook or anything like that. Should I store the returned object (root) in some context or somewhere to use it later? Or what should be the best option to call the render? :/
I know how I should do that with classical functional component... But maybe there is some way that I can just refactor a piece of the code instead of the whole component and all its usecases. Maybe there is something I am not aware of (there is definitely thousands of things I am not aware of :D) that should simplify my life...
function modal() {
return (
<div>
...
</div>
)
}
Modal.show = () => {
modalEl = document.createElement('div');
util.destroy(el) => {
ReactDOM.unmountComponentAtNode(el);
el.remove();
}
const childs = props.childs;
REactDOM.render(childs, modalEl);
}
When I was thinking about portals, I thought I will just rewrite the last line of ReactDOM.render to portal like createPortal(childs, modalEl), unfortunately this does not render anything (except modalEl, but no childs inside). The childs are of type ReactNode (using typescript) and they are not empty (because of ReactDOM.render works without any problem).
When I call setMyState within a useEffect hook, my understanding is react should re-run the logic to choose the component (either MyPage or IonSpinner in this case), but MyPage doesn't render unless I switch to a different tab and come back (using Ionic's IonTabs).
I confirmed setMyState is running because it updates other parts of the application (ex. triggers a different useEffect) and I know MyPage isn't being rendered because I'm doing a console.log() on the first line of rendering MyPage and this log doesn't appear.
Can someone help me with why this is happening?
<IonApp>
<IonReactRouter>
<IonTabs>
<IonRouterOutlet>
<Route exact path="/:tab(MyTab)">
{myState ? <MyPage /> : <IonSpinner />}
</Route>
... more code ...
EDIT:
Pretty sure it's a bug in Ionic? After useEffect runs setMyState, react does a render like it's supposed to but then the IonRouterOutlet has no children (none displayed in React Dev Tools).
I traced this back to the ReactRouterViewStack calling getChildrenToRender() which creates const viewItems. This viewItems object is empty {} because the viewStack is empty {}. I don't know how the viewStack is meant to be populated so I'm not sure where to go from here, but I think addViewItem is not running?
When I go to another tab and then back to this tab, everything renders correctly and the children to IonRouterOutlet are shown in React Dev Tools.
Files:
node_modules/#ionic/react/dist/index.esm.js
node_modules/#ionic/react-router/dist/index.esm.js
try adding MyState in the Dependency array of useEffect
sample code
My case: I have a page preloader, but it makes using react and it's to long (first paint). SSR could solve this problem, but it's too difficult (I mean solving this problem by full SSR).
I want to use something like React.hydrate but for one single component.
I have <MyCustomPreloader /> component which renders <div class="loader" />, but it render with a long delay (after loading the page).
My idea: For example, inside index.html I can make <div class="loader" /> which will be visible at first paint. Main problem say <MyCustomPreloader /> that I have already rendered div and he must use it without creating new.
I could find the necessary DOM inside the component and work with it, but this means abandoning React and continue to work directly with the DOM component.
I tried to manually add <div class="loader" /> into <div id="root"></div> and use React.hydrate instead of React.render and it works! But React.hydrate tries to hydrate every components before and after loader and this solution is kludge.
I believe that there is a function that can partially hydrate a single component (say to component "use this DOM" instead of making same new element), but I cannot find it.
For example:
const loader = ReactDOM.someMagicFunction(<MyCustomPreloader />, document.getElementById("loader"))
Example of this kludge: https://codesandbox.io/s/hopeful-meadow-qgz6j Description: I have "pre-rendered" loader in index.html, and react after loooong loading gets it and USED it (this DOM element).
Can I hydrate single component with some DOM element?
I'm using react-rails to add some react componenets to an existing ruby on rails app. I just realized that all the props being passed initially to the component are easily seen if you inspect the component
<%= react_component('ProfileWeeklyWriting', {
languages: #user.languages,
currentUser: #current_user,
responses: #writing_responses,
clapLottie: asset_url('lottie/clap.json'),
clapIcon: asset_url('icons/clap.svg'),
arrowIcon: asset_url('icons/arrow_green.png')
}) %>
But when you inspect the element, allll those variables are shown!
I know I can just do an ajax call from within the component, but is there a way to pass variables to the component initially, without them being shown to the world?
Let's take a bit theory about how it works. When you do classic SPA without any backend engine you usually do something like
ReactDOM.render(<App />, document.getElementByID('root'))
Which simply render the root component on place of <div id="root" />. When you apply Rails templates on the top, you are connecting 2 different worlds. ReactDOM and Rails slim templating engine (probably) but they don't know nothing about each other. ReactRails is really simply routine which does something like this:
1 Inject custom react-rails script to page
Wait for DOM ready
Collect all [data-react-class] elements
Iterate through of them and call ReactDOM with props.
You can think of it like calling several "mini apps" but in fact those are only components (react doesn't distinguish app/component, it's always just a component)
So the code is something like this (I didn't check the original code but I wrote own react-rails implementation for my company)
function init () {
const elements = document.querySelectorAll('[data-react-class]')
if (elements.length > 0) {
Array.prototype.forEach.call(elements, node => {
if (node) {
mount(node)
}
})
}
}
function mount(node) {
const { reactClass, reactProps } = node.dataset
const props = JSON.parse(reactProps || '{}')
const child = React.createElement(window[reactClass], props)
ReactDOM.render(child, node)
}
Then the DOM ready
document.addEventListener('DOMContentLoaded', e => {
init()
})
Son in fact Rails doesn't know anything about React, and React doesn't know anything about Rails unless it's not living on window. (THIS METHOD IS HIGHLY DISCOURAGED.
In real world there are ways how to make "server" rendering, which means that this piece of code is done on server to not expose props and whole React lifecycle and just flush real prepared HTML to DOM. That means that in the lifecycle BEFORE HTML is sent to the client, there is called transpiler which compiles those components, you can read about it here
https://github.com/reactjs/react-rails#server-side-rendering
So it just calls those methods with a help of https://github.com/rails/execjs
So the only way how to "not expose" props to the client is to "pre-render" components on backend by some engine (either some JS implementation for your language or directly node.js backend). I hope I gave you a better picture how it works and how it can be solved!
I need to redirect a page from another one on server side using the react-router.
The code I wrote it's working on client side, but not in the server render.
you can find the code here:
https://github.com/jurgob/iso-login/blob/e26af0152896a949435db62549027b2683276db7/src/shared/components/LoginPage.js
this is the redirect code inside /src/shared/components/LoginPage.js:
componentWillMount() {
...
this.props.history.replaceState(null, '/home');
}
Note:
If you look on https://github.com/jurgob/iso-login/blob/e26af0152896a949435db62549027b2683276db7/src/shared/routes.js
I did:
function requireAuth(nextState, replaceState) {
// replaceState({ nextPathname: nextState.location.pathname }, '/login');
}
...
<Route path="home" component={HomePage} onEnter={requireAuth} />
this code is working, but I would like to do the redirect inside the component
There's no built-in capacity in React Router to handle redirects within the component on the server side.
This is because onEnter hooks run within the context of match, so React Router can pick up on replaceState calls and notify the match callback of the requested transition. By the time componentWillMount runs, match has already invoked the callback.
You're most likely going to have to build some higher-level abstraction that instruments the history.replaceState call when rendering on the server, then takes the appropriate actions after ReactDOM.renderToString returns.
A couple different ways to handle this:
1 - If you're okay with letting the redirect actually happen on the client side, you can do something like
history.push('/newPath/');
This is the solution I used, in order to not have two different kinds of redirects (on the client and on the server). In my case, I passed in a "context" props (only for the server side portion of the code, so my method looks more like
componentWillMount() {
if (this.props.context) { // context only available on server side
this.props.context.doLogout();
}
this.props.history.push('/newPath/');
}
2 - If you really want the server to do the redirect, then you have to pass down the response object from express or whichever framework you're using:
componentWillMount() {
// you may have to this.props.history.push('/currentPath');
this.props.res.redirect(302, '/newPath/');
}
Happy to elaborate if necessary - I spent some time banging on this and opted for the former solution (code simplicity over correctness, but whatever works for you).