How to use React components as part of legacy HTML/JS app? - javascript

I have an old web application running with HTML/JS.
I have created a new react application and my goal is to start developing new features / pages using React, and implement them in the current application.
The main goal is to use React components outside react, for example:
<html>
<head>
<title> My old web applciation</title>
// Imports for my react app / components
</head>
<body>
<!-- Old staff -->
<Footer someProp="1"/>
</body>
</html>
What I have so far:
For full-page features I used Iframe (and created a route in React for it)
Using element ID, e.g::
and in react:
const GiveItem = ({ ...props }) => {
console.log("init give item component");
return (
<section>
Give Item
</section>
);
};
try {
const domContainer = document.querySelector('#giveItem');
render(e(GiveItem), domContainer);
} catch (e) {
console.error('Failed to render give item', e);
}
export default GiveItem;
Is there any legitimate way to "export" react components to HTML (outside react) and use them as native components like:
<GiveItem someProp="1"/>
Using Vue I managed to do it, just by wrapping the highest-level element with:
<div id="vapp">
.... My old code
<VueComponent />
So back to the question - How I can show/use React components in different parts of legacy app as native HTML components?
Thanks!

I will divide my answer into 3 parts:
simple inclusion of react in general
make your example work
modules
(I will use yarn below. You can use npm as well, I assume you are familiar with these.)
1. simple inclusion of react in general
See Add React to a Website.
index.html:
<html><head>
<title>My old web applciation</title>
</head><body>
<h1>old stuff</h1>
<p>Some old HTML content.</p>
<!-- add a container, where you want to include react components -->
<div id="injected-react-content"></div>
<!-- import the react libraray -->
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<!-- import your react component -->
<script src="GiveItem.js"></script>
</body></html>
GiveItem.js:
(modified from JSX to JS. For JSX see section 2.)
const GiveItem = (props) => {
console.log("init give item component");
return React.createElement(
"section",
null,
"Give item content"
);
};
const domContainer = document.querySelector('#injected-react-content');
ReactDOM.render( React.createElement(GiveItem), domContainer );
2. make your example work
Your component uses JSX. You need to transpile it to JS, using babel.
GiveItem.jsx:
(it is not necessary to call it .jsx, but it makes sense)
const GiveItem = (props) => {
return (
<section>
Give Item
</section>
);
};
const domContainer = document.querySelector('#injected-react-content');
ReactDOM.render( React.createElement(GiveItem), domContainer );
Install babel:
yarn add #babel/cli #babel/core #babel/plugin-transform-react-jsx #babel/preset-react --dev
Transpile your JSX file ./src/GiveItem.jsx into a JS file in ./dist/GiveItem.js:
yarn babel ./src/GiveItem.jsx --presets=#babel/preset-react --out-dir dist
(the dist folder will be created if it doesn't exist)
If you now copy the index.html into ./dist,
you should have the same code as in section 1.,
and opening ./dist/index.html in the browser should work.
3. modules
Of course, you would want to import other react components and sub-components.
(And you probably don't want to import everything individually with <script> tags.)
if an environment is set up already
Maybe your old app already runs in a yarn (or npm) environment, then you might be fine with
install yarn add react and yarn add react-dom
removing the two <script src="https://unpkg.com/react... lines (as react is probably already imported inside your components),
then import some root component (instead of GiveItem.js before),
where you do the ReactDOM.render(...) and import further modules:
E.g.:
index.html:
<!-- import your react component -->
<script type="module" src="EntryIntoReactWorld.js"></script>
EntryIntoReactWorld.js:
import SomeComponent from './SomeComponent';
import SomeOtherComponent from './SomeOtherComponent';
const ReactRoot = props => {
return React.createElement(
"div",
null,
React.createElement(SomeComponent, null),
React.createElement(SomeOtherComponent, null)
);
};
ReactDOM.render(
React.createElement(ReactRoot),
document.querySelector('#injected-react-content')
);
if environment is not set up already
If your old app is not already running in a yarn (or npm) environment, you have to set one up.
Unfortunately, this is not trivial.
(I tried to write a short working minimal example here, but I failed.)
For that you have to enter the world of Javascript modules.
But modern browsers don't allow many things from the local file system.
(e.g. see CORS problems, MIME problem, ...)
You have to install a server for development. You might start with e.g. http-server, but there you probably still will have problems, so I suggest to already start with writing a server.js file and use node http.
(Maybe expressjs as well. It is not necessary, but everybody uses it, and it is easier to find help and tutorials.)
You also might need webpack.
Of course you also have to install yarn add react and yarn add react-dom.

Related

Vue 3 component incorrectly initialized when module is `npm link`ed

Following is the entry point to my library, it generates a component with a dynamic tag:
// muvement.js
import { defineComponent, ref, onMounted, h } from 'vue';
const createMuvement = (tag) => {
return defineComponent({
name: `m-${tag}`,
setup(props, context) {
const root = ref(null);
onMounted(() => {
console.log(root.value);
});
return () => h(tag, { ...context.attrs, ref: root }, context.slots);
}
});
};
const muvement = (...tags) => {
const components = {};
tags.map((tag) => (components[`m-${tag}`] = createMuvement(tag)));
return components;
};
export { muvement };
It's expected to be consumed like so:
// Home.vue
<template>
<div>
<m-div>div</m-div>
<m-button>button</m-button>
</div>
</template>
<script>
import { muvement } from "muvement";
export default {
name: "Home",
components: {
...muvement("div", "button")
}
};
</script>
This works as expected when the library code is contained within the Vue app folder (assuming we are now importing from "#/components/muvement.js" instead of "movement").
That is:
-muvement-test-project (scaffolded with vue-cli)
- src
- views
- Home.vue
- components
- muvement.js
I've also published an alpha release that works fine when importing "muvement" after installing it directly from the npm registry (that is, npm install muvement instead of npm link muvement).
The Problem
During development, I want an app to test the library with that is separate from the library's directory.
I've used npm link to link the library to the test app (as I have done with many other projects in the past).
From /path/to/library
$ npm link
From /path/to/test/app
$ npm link muvement
So far so good. The module is available as a symlink in the test app's node_modules folder. So I import { muvement } from "muvement", run npm run serve, and... BOOM.
Everything explodes (see errors below). It's also probably worth noting that trying to import from the full path (i.e. C:/dev/npm/muvment/dist/es/index.js) results in the same issues as npm link does, so I don't think it has anything to do with the symlink directly.
This is what appears in the console:
For pretty much the entire day I have been trying to solve this one issue. I've seen several seemingly similar questions that were solved by settings Webpack's resolve.symlinks to false but that has no effect on my problem. I've read all through the docs and even Vue's source code (here is the offending line for those who are curious).
Since the warning suggests that the error is commonly attributed to async setup I thought maybe webpack was doing something weird that would make my code async. This doesn't seem to be the case as the call stack of both the working attempt and failed attempt are identical.
What's not identical is the scope.
Here is the scope for the example that is working:
And here is the failing one:
(Notice that the target parameter is null during the call to injectHook, which is obviously what prompts Vue to show a warning).
My question is, why does the location of the imported module make such a difference during the execution of the said module?
The library code and build setup are available here:
https://github.com/justintaddei/muvement
The test app is available here:
https://github.com/justintaddei/muvement/tree/example
If I've left out something important, please let me know in the comments. It's been a long day so I'm sure I've probably missed something.
Thank you.
The problem is your app is using two different vue dependencies under the hood - vue requires the same dependency to be used to keep track on reactivity, lifecycle, etc.
When you link a library npm/yarn will use that linked folder node_modules, but your app is using it's dependencies from it's node_modules.
When your app imports vue it will go app/node_modules/vue but when you import from your linked dependency it will be going to linked_dep/node_modules/vue.
app
node_modules
vue
linked library
node_modules
vue
One easy way to debug this issue is to change both vue dependency files with a console.log and check if the console is logging both.

Creating a DatePicker from CDN with React API

I am importing some React modules from CDN (that's not a requirement, I've also tried with a local build, more in the final question about it):
<script crossorigin src="https://unpkg.com/react-onclickoutside#6.9.0/dist/react-onclickoutside.min.js"></script>
<script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.0/moment.min.js" integrity="sha512-Izh34nqeeR7/nwthfeE0SI3c8uhFSnqxV0sI9TvTcXiFJkMd6fB644O64BRq2P/LA/+7eRvCw4GmLsXksyTHBg==" crossorigin="anonymous"></script>
<script crossorigin src="https://unpkg.com/react-datepicker#3.1.3/dist/react-datepicker.min.js"></script>
Then I have a script to build the React DatePicker component, this is the relevant snippet from it:
HelloWorld.Example=function()
{
var p,setCount,count,p$1,c,myDate,datePicker;
p=React$2.useState(0);
setCount=p[1];
count=p[0];
p$1=React$2.useState(new moment(new Date((c=Date.now(),DateUtil.DatePortion(c)))));
myDate=p$1[0];
datePicker=React$2.createElement(DatePicker.default,{
selected:new moment(new Date()),
onChange:p$1[1]
});
React.set_setCount(setCount);
return React$2.createElement("div",null,datePicker,React$2.createElement("p",null,(Html.textf(function($1)
{
The error that I see from the JS Console is:
react-datepicker.min.js:1 Uncaught TypeError: o is not a function
at Ee (react-datepicker.min.js:1)
when the script call ReactDOM.render.
Is there a way to understand what is o ? Maybe an import missing?
(Edit Well, looking at chrome debugger and comparing it to github, o is isValidDate, i.e. import isValidDate from "date-fns/isValid";, hence the imports from date-fns are not working from CDN )
Is there a way such that - for example - I can locally npm run build the needed module, react-datepicker, and then call the react API from my script as shown above? (a suggestion that I received was configuring my script as entry in webpack, but afaik React doesn't use webpack, though I see it is used in react-datepicker).
From React docs, I can read that
JSX is not a requirement for using React
so something like the above should be doable, in theory.
I've opened a question/issue on github react-datepicker repo (in the context of calling this component from WebSharper.React).
Is there a way such that - for example - I can locally npm run build the needed module, react-datepicker, and then call the react API from my script as shown above?
Yes, there is a well known solution!
Write an index.js as follows
import React from "react";
import DatePicker from 'react-datepicker'
import "react-datepicker/dist/react-datepicker.css";
export {ImportedComponent}
window.MyDatePicker = function MyDatePicker(props) {
console.log("props from window.MyDatePicker", props)
return React.createElement( DatePicker, props );
}
build via npm and copy the static folder from the build of your by npm run build to the SPA folder of your proj
copy
the 3 script tags from the index.html in the build into the index.html template of your proj
and
<div id="root"></div>
(of course you use a different id for your project app and
there will be nothing to render here)
in my case they are (they will be different for you)
<div id="root"></div>
<script>!function(e){function t(t){for(var n,l,p=t[0],f=t[1],i=t[2],c=0,s=[];c<p.length;c++)l=p[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(a&&a(t);s.length;)s.shift()();return u.push.apply(u,i||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,p=1;p<r.length;p++){var f=r[p];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=l(l.s=r[0]))}return e}var n={},o={1:0},u=[];function l(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,l),r.l=!0,r.exports}l.m=e,l.c=n,l.d=function(e,t,r){l.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,t){if(1&t&&(e=l(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(l.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)l.d(r,n,function(t){return e[t]}.bind(null,n));return r},l.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(t,"a",t),t},l.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},l.p="/";var p=this.webpackJsonpcontent_npm=this.webpackJsonpcontent_npm||[],f=p.push.bind(p);p.push=t,p=p.slice();for(var i=0;i<p.length;i++)t(p[i]);var a=f;r()}([])</script>
<script src="/static/js/2.a6e4c224.chunk.js"></script>
<script src="/static/js/main.b075c560.chunk.js"></script>
Now go with
datePicker=React$1.createElement(window.MyDatePicker,{
selected:myDate,
onChange:p$1[1],
showTimeSelect: true,
});
in you SPA.js and enjoy any react component like this one from WebSharper.React!
Btw I had to pass a JS date, not a Moment date here in the selected of props, I'm not sure why, anyway, this is not relevant to the problem.
FYI, this is the F# code from WebSharper project
let myDate, setMyDate = WrapReact.UseState (DateTime.Today.JS)
let importDatePicker = JS.Eval("window.MyDatePicker") :?> React.Class
let propDP =
{
selected = myDate
onChange = setMyDate
showTimeSelect = true
}
let datePicker =
React.CreateElement( importDatePicker, propDP)
WrapReact.setCount <- setCount
div [] [
datePicker
p [] [Html.textf "You selected %s date %s time" (myDate.ToDateString()) (myDate.ToTimeString())]
Full open source project shared on github.
I think that the main problem is that WebSharper scripts are not JavaScript modules. In that case it should be immediate to import an external module or make the above SPA.js as the Webpack main entry. In fact it is well known that there are differences between <script type=module> and <script>
Module Script Execute in Strict Mode
Module Script has its Own Scope
Module Script can Import other Javascript Modules
Module Script has this as Undefined
Inline Module Script can have async Attribute
Module Script is Always Deferred
As confirmed by Adam Granicz indeed on WebSharper side:
that should be the way, yes, #Jand42 and others have been working on changing the current output to support modules and a better TS interoperability - this has been on the agenda for years, so closing it would be a good step forward
(In the meantime there are of course alternatives, e.g. flatpickr, which has bindings also for jQuery, instead of react-datepicker or pure React or F# Fable instead of WebSharper.React and so on)

Convert string React Component to jsx again

I have one question, because read javascript file from NodeJS and send to the client and here I reveive all file as string (which is obvious) and here is my question. Is any solution to convert string component again to the jsx? This is no only html tags so dangerouslySetInnerHTML or similar methods don't work.
My string components looks like typical React component, something like that:
import React from 'react';
import { Row } from 'react-bootstrap;
import Home from './Home'
......
const Index = () => {
const renderHelloWorld = <h1>Hello World</h1>
return (
<div>{renderHelloWorld}</div>
)
}
export default Index;
So this is string I'm struggling with how convert it to jsx (maybe this is impossible) and I should use Server Side Rendering with React methodfs like ReactDOMServer?
You can use just plain old JavaScript to do the trick.
document.querySelector('#elementToBeReplace').innerHTML = renderHelloWorld;
Another Option with react.js is use of dangerouslySetInnerHTML.
<div dangerouslySetInnerHTML={{ __html: renderHelloWorld }} />
Or You can use html-react-parser.
import Parser from 'html-react-parser';
const renderHelloWorld = <h1>Hello World</h1>
<div>{Parser(renderHelloWorld)}</div>
Try this library https://www.npmjs.com/package/react-html-parser.
A utility for converting HTML strings into React components. Avoids the use of dangerouslySetInnerHTML and converts standard HTML elements, attributes and inline styles into their React equivalents.
So, solution for my problem is very popular and simple (in early project stage), to understand problem and fix it we need to go back to the server part of the app. For React applications if we want render jsx file from the server, we have to use server side rendering and above problem will be gone. Maybe I show step by step how enable rendering React component in te server.
Configure backend to enable the ES6 features
Install babel packages
npm install #babel/core #babel/node #babel/preset-env #babel/preset-react --save-dev
There is many ways to configure Babel, I use the nearest package.json for this.
{
......,
/*This have to be in the top level in package.json*/
"babel":{
"presets":[
"#babel/preset-env",
"#babel/preset-react"
]
}
}
More information about babel packages and configurations: https://babeljs.io/docs/en/
Send React component to the client
For this we have to install react and react-dom packages in server side
npm install react react-dom
For example in your server,js or route.js file:
.... //necesarry imported modules
import ReactDOMServer from 'react-dom/server'
import Index from './Index';
router.get('/exampleRoute', (req, res) => {
.... //your route business logic
res.send(ReactDOMServer.renderToString(<Index/>))
})
Render view in the client
...//correct React component
const[state, setState] = useState({Component: ''});
fetch('/exampleRoute')
.then(response => response.json())
.then(data => setState(state => ({...state, Component: data.data));
....
return(
<div dangerouslySetInnerHTML={{ __html: state.Component }}></div>
)
This is only simple example how you can render React component from backend if this is necasarry. This is not guide for complex server side rendering in application which is more complicated thing, but no difficult thing.

Conditionally import assets in create-react-app

Is it possible to conditionally import assets when creating a React app using create-react-app? I'm aware of the require syntax - example:
import React from "react";
const path = process.env.REACT_APP_TYPE === "app_1" ? "app_1" : "app_2";
const imagePath = require(`./assets/${path}/main.png`);
export default function Test() {
return (
<img src={imagePath} alt="" />
);
}
This however bundles all my assets no matter what.
It will load the proper image, but it will still bundle all the files together in the final build.
When I look in the dev tools for the final build, I can see all the assets there even though I only wanted to load the assets for app_1.
Am I forced to touch the webpack config, if so, what should I change? or is there another way?
In the days when React didn't exist we didn't put assets into our JS files. We let the CSS to decide, what assets to load for what selectors. Then you could simply switch a corresponding class on or off for a corresponding element (or even the whole page) and viola it changes color, background, or even a form. Pure magic!
Ah. What times these were!
All above is true and I do not understand why would anyone do or recommend doing it differently. However if you still want to do it (for any reason) - you can! Latest create-react-app comes with out-of-the-box support for lazy loading of arbitrary components via dynamic importing and code splitting. All you need to do is use parenthesized version of the import() statement instead of the regular one. import() takes in a request string as usual and returns a Promise. That's it. Source code of the dynamicaly requested component won't be bundled in, but instead stored in separate chunks to be loaded on demand.
Before:
import OtherComponent from './OtherComponent';
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}
After:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}
Notice how function MyComponent part is identical.
For those wondering if it is tied to CRA or React, it's not. It's a generic concept that can be used in vanilla JavaScript.
You will need to use webpack (or other bundler.) The code is not being run when it's bundled, so the compiler has no way of knowing which branch of logic to follow (app_1 or app_2). Therefore you have to get into the bundler's logic in order to achieve your goal.
However, this isn't as scary as it seems since webpack has built in capability to do this (no 3rd parties required...)
I would look into using webpack.providePlugin
(https://webpack.js.org/plugins/provide-plugin)
or its sibling DefinePlugin
(https://webpack.js.org/plugins/define-plugin)
(I'm afraid these examples are off the top of my head, so it's very unlikely they'll work on first pass.)
Examples:
Both will require a provider module...
// in path/provider.js
module.exports = {
live: '/path/to/live/image',
dev: '/path/to/dev/image'
}
Provide Plugin Example
// in webpack
new webpack.ProvidePlugin({
imagePath: [
'path/provider', // the file defined above
process.env.ENVIRONMENT // either 'dev' or 'live'
]
}),
// in code
export default function Test() {
return (
<img src={imagePath} alt="" />
);
}
Define Plugin example:
// in webpack
new webpack.DefinePlugin({
'process.env.ENVIRONMENT': JSON.stringify(process.env.ENVIRONMENT)
});
// in code
var providers = require('path/provider'); // same path provider as above
export default function Test() {
return (
<img src={providers[process.env.ENVIRONMENT]} alt="" />
);
}
In both cases the bundler is forced to collapse your variable to an actual literal value at compile time - before bundling has taken place. Since you have now collapsed the logical path down to a single option, it is now free to only bundle the relevant assets.
You can't do this with default CRA settings.
Because if your dynamic require or dynamic import path is not static, webpack won't be able to determine which assets to include in the final build folder, therefore, it will grab everything from your ./src folder, and put them all to your build folder.
There is a way to do it with default CRA settings
You can add to .env something like
REACT_APP_SKIN=1
Put your skin assets in public/css1, public/css2 etc. And include them in public/index.html using code like
<link href="/css%REACT_APP_SKIN%/theme.css" rel="stylesheet">

How to load a component separately from webpack bundles javascript?

I want to load a component to my React application externally from my webpack bundled file. To elaborate my point, here is a sample HTML file that I want to have:
<!doctype html>
<html>
<head> ... </head>
<body>
<div id="app"></div>
<script src="/dist/app.js"></script>
<script src="/external-component/timer-component.js"></script>
</body>
</html>
I basically want to be able to load timer component but still make that component to be dependent on app.js (webpack bundled file) since app.js has React. In other words, I want timer component to know that React exists. My current timer-component file is the following (taken from ReactJS website):
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = { secondsElapsed: 0 };
}
tick() {
this.setState(prevState => ({
secondsElapsed: prevState.secondsElapsed + 1
}));
}
componentDidMount() {
this.interval = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return React.createElement(
"div",
null,
"Seconds Elapsed: ",
this.state.secondsElapsed
);
}
}
I keep getting error that React doesn't exist. If I try to import React:
const React = require('react');
// or
import React from 'react';
I get an error that require (or import) is undefined. What's the proper way to do it? My understanding is that webpack app.js file kind of creates namespaces, so React component does not really see the outside world. Only the components inside webpack file see React. If this is the case, how can I expose react to the outside world?
Somewhere in your app.js, declare react as a global variable.
window.React = React
This way you can access it from other components.
Another way to do this would be externalizing react from the webpack build and including it via a script tag in the page.
To externalize react, include this in your webpack config
externals: {'react': 'React'}
Include this in your html to access react globally.
<script src="https://unpkg.com/react#15/dist/react.js"></script>
<script src="https://unpkg.com/react-dom#15/dist/react-dom.js"></script>
I think you want to use the "script-loader" plugin for Webpack. I use it to do just this and it works great.
https://github.com/webpack/script-loader
It's a great way to keep all the stuff out of index.html and put all your requires in one place, e.g. my seed project index.js has all this in it, and in other projects all I have to do is add scripts here when I need. They don't even have to be npm installed, you can just have a script in your project and load it into the bundle this way.
(One of my projects using webpack's script-loader)
require('!!script!core-js/client/shim.js');
require('!!script!zone.js/dist/zone.js');
require('!!script!reflect-metadata/temp/Reflect.js');
require('!!script!rxjs/bundles/Rx.js');
require('!!script!#angular/core/bundles/core.umd.js');
require('!!script!#angular/common/bundles/common.umd.js');
require('!!script!#angular/compiler/bundles/compiler.umd.js');
require('!!script!#angular/platform-browser/bundles/platform-browser.umd.js');
require('!!script!#angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js');
require('!!script!#angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js');
require('!style!css!./css/styles.css');
require('!style!css!./css/animate.css');
require('!style!css!bootstrap/dist/css/bootstrap.css');
require('./app/main');

Categories

Resources