How safely to pass params to react component with react-rails - reactjs

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!

Related

Alternative to Reactdom.render and unmountComponentAtNode in react18

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

How to lazy-load a React "widget"?

My terminology is probably wrong here, but I don't know what to call it other than a "widget" when you don't have a whole React app, but are attaching little React pieces to different roots on an otherwise static HTML page. But, that's what I'm doing:
const StripeForm = React.lazy(() => import('./Stripeform'));
// ...
const formPlaceholder = document.getElementById('cwr-stripe-form');
const formRoot = createRoot(formPlaceholder);
formRoot.render(
<React.StrictMode>
<StripeForm />
</React.StrictMode>
);
The problem I'm trying to solve is, I want to lazy-load my StripeForm, so that it doesn't load until needed (and therefor maybe never at all).
I have the React.lazy import and my webpack bundle working fine. The issue is "until needed".
As I understand it, lazy-loaded components load when they begin rendering, which I suppose is usually when they move into the viewport. And I equally suppose that "usually" is overridden by the fact that I'm calling .render, which I guess causes it to render immediately.
Those guesses might be wrong, but the fact is, when I load my web page, the supposedly lazy-loaded component is loaded, even if it's not in the viewport.
How to I get these sort of "widgets" (there are several others on the page) to load lazily (i.e., to attach to my root placeholder element, but not actually load and render until necessary)?
You're already using lazy, so React will only import the component if it's not being rendered. The problem is that you're still rendering the component by default, so the component is still being loaded once it's available.
React is declarative, so the way to solve this is to conditionally render the component only when you want it to be rendered. You can implement this by using a visibility library such as react-is-visible, for example:
import React, { useRef } from 'react'
import { useIsVisible } from 'react-is-visible'
function LazyStripeForm() {
const ref = useRef()
const isVisible = useIsVisible(ref, { once: true })
return <div ref={ref}>{isVisible && <StripeForm />}</div>
}
Now you can render LazyStripeForm instead of StripeForm and it should do what you want.
Also, if StripeForm has a lot of dependencies, you should ensure your build tool is code splitting the file so that it's not increasing the size of your main bundle unnecessarily.

React / Context API / TypeScript : How to init web app and avoid UI flickering at statup?

When a user navigate to my site, I need to initialize the React web app as follow:
If the incoming user has never requested the web app, I want to set the UI language to browser default (navigator.language).
If the incoming user has already visited the site and chosen a prefered language (lang stored in the localStorage), I want to init the UI with this language.
If the incoming user has an account and is already connected (token available in localStorage), I want to auto-connect him and render the app accordingly : login button transformed into a welcome message, UI language set to user preference.
To do so, I'm using React Context API and a defaultUser object.
defaultUser: init a default user
const defaultUser = {
language: 'en_EN',
isConnected: false
}
Context: create a default context
export const AppContext = createContext({
connectedUser: defaultUser,
})
Provider: create the provider with default context
export function AppProvider({ children }: any) {
[...]
const provider = {
connectedUser
}
return (
<AppContext.Provider value={provider}>
{children}
</AppContext.Provider>
)
}
App: init the provider during app start up
export class App extends Component {
static contextType = AppContext
render() {
return (
<AppProvider>
<AppContainer />
</AppProvider>
)
}
}
AppContainer: render the app
export class AppContainer extends Component {
static contextType = AppContext
componentDidMount() {
/** If user is not connected, verify if a stored session exists and use it to connect user */
if (!this.context.connectedUser.isConnected) {
[...do things...]
}
}
The whole mecanism works well except an annoying thing : the web app is systematically initialized with default user values, until the AppContainer::componentDidMount() do the real init job.
This is causing a sort of flickering effect.
I'm struggeling for 2 days on how to fix that, trying to perform Context init before <AppContainer /> rendering, and I'm stuck.
Any recommandations?
EDIT :
For clarity, I'm adding a diagram. Currently :
React App is rendered at start.
Context is initialized at start with default value.
Context is updated when end is reached.
React App is rendered again when end.
Any layout change during these two steps (UI language, UI modification based on user permissions) are clearly visible to the user and generate a sort of flickering.
I found sort of a solution by simply conditionning <AppContainer/> loading, postponing it to the end of the sequence. However instead of having flickering I have now a lag and other unwanted side effects.
The goal would be to differ all the sequence before React Act is rendered, and after Window is available. Then dynamically create the Context, then render all.
I think the point would be resolved if I could dynamically create the AppContext and pass a variable to createContext() during App constructor() or maybe componentWillMount() (not sure when Window is available), but then TypeScript get into play with types issues and I'm still stuck.
You didn't share the code that initializes the context, but I suspect you put the default value to be either a hardcoded value, or navigator.language and therefore experience the flickering. I'd like to suggest two ways to solve this:
Solution 1
Perhaps instead of having a hardcoded default context you could generate the default context programmatically by accessing localStorage.get('lang') or similar? There is a slight drawback to this solution though: You will be mixing concerns of react and the browser, but I think in this case it's an alternative to consider, because it's very simple and obvious to the reader.
Solution 2
Alternatively, when calling ReactDOM.render you could pass down whatever you need from localStorage as a prop to your application and so you keep the browser related logic separate from the pure React stuff.
I hope this makes sense.
Here's my follow-up after Amit suggestions, in case it can help anyone else.
Init Context with functions
Instead of initializing defaultUser with hard-coded values and update it later, I set directly it with a function returning navigator.lang as suggested. This solved the flickering issue on UI labels.
Init data before RectDOM.render
However I still had flickering on UI components for which I have to get the appropriate state from an API call.
Eg, if the incoming user has a valid session token stored in localStorage, the Login button must be disabled. Before doing so, I need to make sure the session token is valid by an async call to the API. I didn't find a way to have it «awaited» by the Context init which seems to be synchronous.
That's where Amit second suggestion get into play. Instead of struggling finding a solution inside React, I did necessary processing before ReactDOM.render, then passing stuffs as props to <Apps/>.
This works pretty well to get and pass the data...
Except that Context API didn't setSate anymore as soon as any of its data was refering to an object from outside the Context. In other word using function calls is ok to init (probably by val), but reference to external objects breaks setState.
Conclusion
As my project is still in early stage, this gave me the chance to get rid of Context API, do the proper init as required, and code the props/states progagation with basic React.

Using this.props.history.push("/path") is re-rendering and then returning

Edited the question after further debugging
I am having a strange issue, tried for a while to figure it out but I can't.
I have a React Component called NewGoal.jsx, after a user submits their new goal I attempt to reroute them to my "goals" page.
The problem: After they submit the browser loads in my goal page, but only for one second. It then continues and goes BACK to the NewGoal page!!
I am trying to understand why this is happening, I am beginning to feel that this might be an async issue.
Here is my code, currently it is using async-await, I also tried the same idea using a .then() but it also didn't work:
async handleSubmit(event)
{
const response = await axios.post("http://localhost:8080/addGoal",
{
goalID: null,
duration: this.state.days,
accomplishedDays: 0,
isPublic: this.state.isPublic,
description: this.state.name,
dateCreated: new Date().toISOString().substring(0,10),
}) */
// push to route
this.props.history.push("/goals");
}
While debugging, I tried taking out the functionality where I post the new message, and just did a history.push, code is below - and this completely worked.
// THIS WORKS
async handleSubmit(event)
{
// push to route
this.props.history.push("/goals");
}
But as soon as I add anything else to the function, whether before the history.push or after, it stops working.
Any advice would be very very appreciated!
Thank you
In the React Router doc's the developers talk about how the history object is mutable. Their recommendation is not to alter it directly.
https://reacttraining.com/react-router/web/api/history#history-history-is-mutable
Fortunately there are few ways to programmatically change the User's location while still working within the lifecycle events of React.
The easiest I've found is also the newest. React Router uses the React Context API to make the history object used by the router available to it's descendents. This will save you passing the history object down your component tree through props.
The only thing you need to do is make sure your AddNewGoalPage uses the history object from context instead of props.
handleSubmit(event)
...
//successful, redirect to all goals
if(res.data)
{
this.context.history.push("/goals")
}
...
})
}
I don't know if you're using a class component or a functional component for the AddNewGoalPage - but your handleSubmit method hints that it's a member of a Class, so the router's history object will be automatically available to you within your class through this.context.history.
If you are using a functional component, you'll need to make sure that the handleSubmit method is properly bound to the functional component otherwise the context the functional component parameter is given by React won't not be available to it.
Feel free to reply to me if this is the case.

I don't know how to organize a web app that uses component based web framework

I am trying to learn component based frameworks for frontend apps. Currently, I am using RiotJS but it applies to any framework that uses the same concepts (React, Angular 2.0 etc).
In a basic MVC frontend frameworks (e.g AngularJS), the controllers and router were very connected to each other. But with a component based framework, the line between router and controllers is much wider. And this is what confuses me the most.
Here is one example of an app that I am trying to build:
I have three main UI elements: Navigation Bar, Content Area, and Signin Form. So, I created three components: my-navbar, my-content, my-signin. I was able to create multiple routes per component. So for example, if there is a route changes, the navbar updates the active "module." Making this was easy because all I am doing is changing class of a list item.
Now, I want to load other tags inside <my-content></my-content>. In AngularJS, I was always changing the view completely (using ui-router). How can I achieve that in a component based framework. Let's say that I have 2 more components called my-content-users-list-view, my-content-users-detail-view. How can I add them to the component my-content based on the route? Do I just add it like document.innerHTML += '<my-content-users-list-view></my-content-users-list-view>?
I know most of my syntax is RiotJS but I will understand it if you write it in another framework's syntax.
Thank you!
Essentially, yes, you could just append your tag as a DOM node and then call Riot to mount it:
riot.route('/awesome-route', () => {
const tag = 'your-awesome-tag';
const options = { ... };
const elem = document.createElement(tag);
// TODO empty your content container using pure DOM or jQuery to get rid of the previous route's view...
document.querySelector('#content').appendChild(elem);
riot.mount(elem, tag, options);
});

Resources