How to test the component refresh behavior with react-testing-library? - react-testing-library

I'm writing a test for a component that takes a few props such as "isLoading", "clients" and "refreshClients". refreshClients is defined like this:
function refreshClients() {
setIsLoading(true)
getClients().then(response => {
setClients(response)
setIsLoading(false)
})
}
clients and isLoading are states from the parent component that are used as props of the child component. The client component also gets the refreshClients prop, which allows it to update its own props (isLoading and clients), through the function passed in by the parent component.
This is one of the use cases: after taking an action over a client, like deletion, the component will call refreshClients, which should take care of reloading the clients table displayed in the component. While the new listing is being loaded the table isn't displayed (isLoading is true). The component works well, however I'm unsure on how to properly test it using testing-library. I'm basically calling rerender in the tests but I feel there should be a way to replicate this behavior in the tests...
Is there a way to create states to pass as props to the tested component? Or is there another recommended approach to handle cases like this?
In case it makes it easier to visualize the idea, here is a complete simple example of how it would work:
import {createRoot} from 'react-dom'
import React, {useState} from 'react';
export function App() {
const [isLoading, setIsLoading] = useState(false);
const [clients, setClients] = useState(['david', 'sara']);
const refreshClients = () => {
setIsLoading(true)
setTimeout(() => {
setClients(['john', 'mary'])
setIsLoading(false)
}, 1000)
}
return <ClientsTable isLoading={isLoading} clients={clients} refreshClients={refreshClients} />
}
function ClientsTable({ isLoading, clients, refreshClients}) {
const deleteClient = () => {
console.log('TODO: delete client')
refreshClients()
}
return (
<div>
{isLoading && <p>Loading... please wait</p>}
{!isLoading && clients.map(client => (
<div>{client} <button onClick={deleteClient}>delete</button></div>
))}
</div>
);
}
createRoot(
document.getElementById('root')
).render(<App />)

Related

Redux toolkit createSlice not working as expected (state is not modified thought action seem to be fired)

I am new to Redux toolkit. I have a working app in which would like to implement it in place of existing "regular" reducer.
import { createSlice, PayloadAction } from "#reduxjs/toolkit";
import { SelectedMinifig } from "types";
const initialState = {} as SelectedMinifig;
const selectedMinifigSlice = createSlice({
name: "selectedMinifigX",
initialState,
reducers: {
setSelectedMinifigX(state, action: PayloadAction<SelectedMinifig>) {
state = action.payload;
console.log("state and action payload from slice", state, action.payload);
},
},
});
export default selectedMinifigSlice.reducer;
export const { setSelectedMinifigX } = selectedMinifigSlice.actions;
Please note that in the code I use postfix "X" to differentiate new names from existing ones.
From the above slice, exports are consumed like this:
import selectedMinifigReducer from "reduxware/reducers/selectedMinifigSlice";
import { partsApi } from "../api/partsApi";
const rootReducer = combineReducers({
fetch: fetchReducer,
selected: selectedReducer,
teasers: teasersReducer,
selectedMinifigX: selectedMinifigReducer,
[partsApi.reducerPath]: partsApi.reducer,
});
Above I consume reducer, and with two files below I consume action (the latest file is my usual workaround not to useDispatch in components directly):
index.ts:
export { setSelectedMinifigX } from "reduxware/reducers/selectedMinifigSlice";
useDispatchAction.ts
import { useDispatch } from "react-redux";
import { bindActionCreators } from "redux";
import { actionCreators } from "reduxware";
const useDispatchAction = () => {
const dispatch = useDispatch();
return bindActionCreators(actionCreators, dispatch);
};
export default useDispatchAction;
The actions are fired like this (the new action is setSelectedMinifigX(selected), the old is setSelectedMinifig(selected), both with the same argument) :
onClick={e => {
e.stopPropagation();
setSelectedMinifig(selected);
setSelectedMinifigX(selected);
history(Paths.order);
}}
And in the moment of firing action, I really receive in console comment "state and action payload from slice " with expected content. That is why I claim action is actually fired.
The problem is that when I reach for state it is still empty object like initial state.
I have a component that is linked with state like below:
const mapStateToProps = (state: RootStateType) => ({
selectedMinifig: state.selected.selectedMinifig,
selectedMinifigX: state.selectedMinifigX,
});
and within this component, selectedMinifigX is an empty object. What is wrong here?
Hmm some of this looks a little foreign / extra to me.
Your reducer looks correct. Inside of your React component try to do something like this:
const SomeComponent = () => {
const dispatch = useDispatch()
const someHandlerFn = (e) => {
e.stopPropagation();
dispatch(setSelectedMinifigX(selected))
history(Paths.order);
}
return <button onClick={someHandlerFn}>Test Me</button>
}
I'm not sure if bindActionCreators is still valid redux. Was something I use to do before redux toolkit when using class based components. You should see your reducer fire inside of your reducer file.
The useDispatchAction.ts seems like extra stuff you don't need.

NEXTJS fix window is not defined on import [duplicate]

In my Next.js app I can't seem to access window:
Unhandled Rejection (ReferenceError): window is not defined
componentWillMount() {
console.log('window.innerHeight', window.innerHeight);
}
̶A̶n̶o̶t̶h̶e̶r̶ ̶s̶o̶l̶u̶t̶i̶o̶n̶ ̶i̶s̶ ̶b̶y̶ ̶u̶s̶i̶n̶g̶ ̶p̶r̶o̶c̶e̶s̶s̶.̶b̶r̶o̶w̶s̶e̶r ̶ ̶t̶o̶ ̶j̶u̶s̶t̶ ̶e̶x̶e̶c̶u̶t̶e̶ ̶ ̶y̶o̶u̶r̶ ̶c̶o̶m̶m̶a̶n̶d̶ ̶d̶u̶r̶i̶n̶g̶ ̶r̶e̶n̶d̶e̶r̶i̶n̶g̶ ̶o̶n̶ ̶t̶h̶e̶ ̶c̶l̶i̶e̶n̶t̶ ̶s̶i̶d̶e̶ ̶o̶n̶l̶y̶.
But process object has been deprecated in Webpack5 and also NextJS, because it is a NodeJS variable for backend side only.
So we have to use back window object from the browser.
if (typeof window !== "undefined") {
// Client-side-only code
}
Other solution is by using react hook to replace componentDidMount:
useEffect(() => {
// Client-side-only code
})
Move the code from componentWillMount() to componentDidMount():
componentDidMount() {
console.log('window.innerHeight', window.innerHeight);
}
In Next.js, componentDidMount() is executed only on the client where window and other browser specific APIs will be available. From the Next.js wiki:
Next.js is universal, which means it executes code first server-side,
then client-side. The window object is only present client-side, so if
you absolutely need to have access to it in some React component, you
should put that code in componentDidMount. This lifecycle method will
only be executed on the client. You may also want to check if there
isn't some alternative universal library which may suit your needs.
Along the same lines, componentWillMount() will be deprecated in v17 of React, so it effectively will be potentially unsafe to use in the very near future.
If you use React Hooks you can move the code into the Effect Hook:
import * as React from "react";
export const MyComp = () => {
React.useEffect(() => {
// window is accessible here.
console.log("window.innerHeight", window.innerHeight);
}, []);
return (<div></div>)
}
The code inside useEffect is only executed on the client (in the browser), thus it has access to window.
With No SSR
https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr
import dynamic from 'next/dynamic'
const DynamicComponentWithNoSSR = dynamic(
() => import('../components/hello3'),
{ ssr: false }
)
function Home() {
return (
<div>
<Header />
<DynamicComponentWithNoSSR />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
The error occurs because window is not yet available, while component is still mounting. You can access window object after component is mounted.
You can create a very useful hook for getting dynamic window.innerHeight or window.innerWidth
const useDeviceSize = () => {
const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0)
const handleWindowResize = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
}
useEffect(() => {
// component is mounted and window is available
handleWindowResize();
window.addEventListener('resize', handleWindowResize);
// unsubscribe from the event on component unmount
return () => window.removeEventListener('resize', handleWindowResize);
}, []);
return [width, height]
}
export default useDeviceSize
Use case:
const [width, height] = useDeviceSize();
componentWillMount() lifecycle hook works both on server as well as client side. In your case server would not know about window or document during page serving, the suggestion is to move the code to either
Solution 1:
componentDidMount()
Or, Solution 2
In case it is something that you only want to perform in then you could write something like:
componentWillMount() {
if (typeof window !== 'undefined') {
console.log('window.innerHeight', window.innerHeight);
}
}
In the constructor of your class Component you can add
if (typeof window === 'undefined') {
global.window = {}
}
Example:
import React, { Component } from 'react'
class MyClassName extends Component {
constructor(props){
super(props)
...
if (typeof window === 'undefined') {
global.window = {}
}
}
This will avoid the error (in my case, the error would occur after I would click reload of the page).
global?.window && window.innerHeight
It's important to use the operator ?., otherwise the build command might crash.
Best solution ever
import dynamic from 'next/dynamic';
const Chart = dynamic(()=> import('react-apexcharts'), {
ssr:false,
})
A bit late but you could also consider using Dynamic Imports from next turn off SSR for that component.
You can warp the import for your component inside a dynamic function and then, use the returned value as the actual component.
import dynamic from 'next/dynamic'
const BoardDynamic = dynamic(() => import('../components/Board.tsx'), {
ssr: false,
})
<>
<BoardDynamic />
</>
I have to access the hash from the URL so I come up with this
const hash = global.window && window.location.hash;
Here's an easy-to-use workaround that I did.
const runOnClient = (func: () => any) => {
if (typeof window !== "undefined") {
if (window.document.readyState == "loading") {
window.addEventListener("load", func);
} else {
func();
}
}
};
Usage:
runOnClient(() => {
// access window as you like
})
// or async
runOnClient(async () => {
// remember to catch errors that might be raised in promises, and use the `await` keyword wherever needed
})
This is better than just typeof window !== "undefined", because if you just check that the window is not undefined, it won't work if your page was redirected to, it just works once while loading. But this workaround works even if the page was redirected to, not just once while loading.
I was facing the same problem when i was developing a web application in next.js This fixed my problem, you have to refer to refer the window object in a life cycle method or a react Hook. For example lets say i want to create a store variable with redux and in this store i want to use a windows object i can do it as follows:
let store
useEffect(()=>{
store = createStore(rootReducers, window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__())
}, [])
....
So basically, when you are working with window's object always use a hook to play around or componentDidMount() life cycle method
I wrapped the general solution (if (typeof window === 'undefined') return;) in a custom hook, that I am very pleased with. It has a similiar interface to reacts useMemo hook which I really like.
import { useEffect, useMemo, useState } from "react";
const InitialState = Symbol("initial");
/**
*
* #param clientFactory Factory function similiar to `useMemo`. However, this function is only ever called on the client and will transform any returned promises into their resolved values.
* #param deps Factory function dependencies, just like in `useMemo`.
* #param serverFactory Factory function that may be called server side. Unlike the `clientFactory` function a resulting `Promise` will not be resolved, and will continue to be returned while the `clientFactory` is pending.
*/
export function useClientSideMemo<T = any, K = T>(
clientFactory: () => T | Promise<T>,
deps: Parameters<typeof useMemo>["1"],
serverFactory?: () => K
) {
const [memoized, setMemoized] = useState<T | typeof InitialState>(
InitialState
);
useEffect(() => {
(async () => {
setMemoized(await clientFactory());
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return typeof window === "undefined" || memoized === InitialState
? serverFactory?.()
: memoized;
}
Usage Example:
I am using it to dynamically import libaries that are not compatible with SSR in next.js, since its own dynamic import is only compatible with components.
const renderer = useClientSideMemo(
async () =>
(await import("#/components/table/renderers/HighlightTextRenderer"))
.HighlightTextRendererAlias,
[],
() => "text"
);
As you can see I even implemented a fallback factory callback, so you may provide a result when initially rendering on the server aswell. In all other aspects this hook should behave similiar to reacts useMemo hook. Open to feedback.
For such cases, Next.js has Dynamic Import.
A module that includes a library that only works in the browser, it's suggested to use Dynamic Import. Refer
Date: 06/08/2021
Check if the window object exists or not and then follow the code along with it.
function getSelectedAddress() {
if (typeof window === 'undefined') return;
// Some other logic
}
For Next.js version 12.1.0, I find that we can use process.title to determine whether we are in browser or in node side. Hope it helps!
export default function Projects(props) {
console.log({ 'process?.title': process?.title });
return (
<div></div>
);
}
1. From the terminal, I receive { 'process?.title': 'node' }
2. From Chrome devtool, I revice { 'process?.title': 'browser' }
I had this same issue when refreshing the page (caused by an import that didn't work well with SSR).
What fixed it for me was going to pages where this was occurring and forcing the import to be dynamic:
import dynamic from 'next/dynamic';
const SomeComponent = dynamic(()=>{return import('../Components/SomeComponent')}, {ssr: false});
//import SomeComponent from '../Components/SomeComponent'
Commenting out the original import and importing the component dynamically forces the client-side rendering of the component.
The dynamic import is covered in Nextjs's documentation here:
https://nextjs.org/docs/advanced-features/dynamic-import
I got to this solution by watching the youtube video here:
https://www.youtube.com/watch?v=DA0ie1RPP6g
You can define a state var and use the window event handle to handle changes like so.
const [height, setHeight] = useState();
useEffect(() => {
if (!height) setHeight(window.innerHeight - 140);
window.addEventListener("resize", () => {
setHeight(window.innerHeight - 140);
});
}, []);
You can try the below code snippet for use-cases such as - to get current pathname (CurrentUrl Path)
import { useRouter } from "next/router";
const navigator = useRouter()
console.log(navigator.pathname);
For anyone who somehow cannot use hook (for example, function component):
Use setTimeout(() => yourFunctionWithWindow()); will allow it get the window instance. Guess it just need a little more time to load.
I want to leave this approach that I found interesting for future researchers. It's using a custom hook useEventListener that can be used in so many others needs.
Note that you will need to apply a little change in the originally posted one, like I suggest here.
So it will finish like this:
import { useRef, useEffect } from 'react'
export const useEventListener = (eventName, handler, element) => {
const savedHandler = useRef()
useEffect(() => {
savedHandler.current = handler
}, [handler])
useEffect(() => {
element = !element ? window : element
const isSupported = element && element.addEventListener
if (!isSupported) return
const eventListener = (event) => savedHandler.current(event)
element.addEventListener(eventName, eventListener)
return () => {
element.removeEventListener(eventName, eventListener)
}
}, [eventName, element])
}
If it is NextJS app and inside _document.js, use below:
<script dangerouslySetInnerHTML={{
__html: `
var innerHeight = window.innerHeight;
`
}} />

Mobx React Form - How to implement Custom onSubmit

I am using mobx-react-from and i have a a problem to figure how i can use an action that i have in my store inside obSubmit hook ....
the mobx form is working ok .. i can see the inputs and the validation
and when i submit the form all i want is to use an action from store ...
my AutStore file :
import {observable,action} from 'mobx';
class AuthStore {
constructor(store) {
this.store = store
}
#action authLogin=(form)=>{
this.store.firebaseAuth.signInWithEmailAndPassword().then(()=>{
}).catch(()=>{
})
}
}
export default AuthStore
my AuthForm File :
import {observable, action} from 'mobx';
import MobxReactForm from 'mobx-react-form';
import {formFields,formValidation} from './formSettings';
const fields = [
formFields.email,
formFields.password
];
const hooks = {
onSuccess(form) {
// Here i want to use an action - authLogin from my AuthStore
console.log('Form Values!', form.values());
},
onError(form) {
console.log('All form errors', form.errors());
}
};
const AuthForm = new MobxReactForm({fields}, {plugins:formValidation,
hooks});
export default AuthForm
i would like to know how can i connect all together thanks !!!
I haven't used mobx-react-form before but have used mobx and react extensively. There's a couple ways to do this. The way I have done it is as follows, assuming Webpack & ES6 & React 14 here. Instantiate the store, and use a Provider around the component that hosts the form.
import { Provider } from 'mobx-react'
import React, { Component, PropTypes } from 'react'
import AuthStore from '{your auth store rel path}'
import FormComponent from '{your form component rel path}'
// instantiate store
const myAuthStore = new AuthStore()
// i don't think the constructor for AuthStore needs a store arg.
export default class SingleFormApplication extends Component {
render() {
return (
<Provider store={myAuthStore} >
<FormComponent />
</Provider>
)
}
}
Your FormComponent class will need to take advantage of both the observer and inject methods of the mobx-react package that will wrap it in a higher order component that both injects the store object as a prop and registers a listener on the store for changes that will rerender the component. I typically use the annotation syntax and it looks like this.
#inject('{name of provider store prop to inject}') #observer
export default class Example extends Component {
constructor(props) {
super(props)
this.store = this.props.store
}
}
Finally, with the store injected, you can now pass an action from the store into an AuthForm method, which I would advise you modify accordingly. Have the AuthForm file export a method that takes an onSuccess method as an arg and returns the mobx-react-form object. I would also modify your store action to simply take the email and password as an arg instead of the whole form. In the FormComponent try this:
import { formWithSuccessAction } from '{rel path to auth form}'
then in constructor after this.store = this.props.store assignment...
this.form = formWithSuccessAction(this.store.authLogin)
then in your render method of the FormComponent use the this.form class variable to render a form as you would in the mobx-react-form docs.
To be as clear as possible, the AuthForm.formWithSuccessAction method should look something like this:
const formWithSuccessAction = (storeSuccessAction) => {
const fields = [
formFields.email,
formFields.password
];
const hooks = {
onSuccess(form) {
// Here i want to use an action - authLogin from my AuthStore
console.log('Form Values!', form.values());
// get email and password in separate vars or not, up to you
storeSuccessAction(email, password)
},
onError(form) {
console.log('All form errors', form.errors());
}
};
const AuthForm = new MobxReactForm({fields}, {plugins:formValidation,
hooks});
return AuthForm
}
Hopefully this helps you on your way.

React/Redux and Websockets for Timer actions

Here is my use case:
I have two different apps, react client app and express/node backend server app having REST APIs. I want the react client app to refresh the component states, every time the Server sends an event on the socket which has change of the data on the server side.
I have seen examples of websocket doing this (http://www.thegreatcodeadventure.com/real-time-react-with-socket-io-building-a-pair-programming-app/) but in this case the client and the server components are in the same app. How to do this when you different apps for client and the server components.
Should I use Timer (https://github.com/reactjs/react-timer-mixin) to make a call from the client to the server rest endpoint and refresh the components on the client if there is any change in data. Or does redux middleware provide those capabilities..
thanks, Rajesh
I think what you are looking for is something like react-redux. This allows you to connect the component to depend on a piece of the state tree and will be updated whenever the state changes (as long as you are applying new references). See below:
UserListContainer.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as UserActions from '../actions/userActions';
import UserList from '../components/UserList';
class UserListContainer {
// Subscribe to changes when the component mounts
componentDidMount() {
// This function
this.props.UserActions.subscribe();
}
render() {
return <UserList {...props} />
}
}
// Add users to props (this.props.users)
const mapStateToProps = (state) => ({
users: state.users,
});
// Add actions to props
const mapDispatchToProps = () => ({
UserActions
});
// Connect the component so that it has access to the store
// and dispatch functions (Higher order component)
export default connect(mapStateToProps)(UserListContainer);
UserList.jsx
import React from 'react';
export default ({ users }) => (
<ul>
{
users.map((user) => (
<li key={user.id}>{user.fullname}</li>
));
}
</ul>
);
UserActions.js
const socket = new WebSocket("ws://www.example.com/socketserver");
// An action creator that is returns a function to a dispatch is a thunk
// See: redux-thunk
export const subscribe = () => (dispatch) => {
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'user add') {
// Dispatch ADD_USER to be caught in the reducer
dispatch({
type: 'ADD_USER',
payload: {
data.user
}
});
}
// Other types to change state...
};
};
Explanation
Essentially what is happening is that when the container component mounts it will dispatch a subscribe action which will then listed for messages from the socket. When it receives a message it will dispatch another action base off of its type with the corresponding data which will be caught in the reducer and added to state. *Note: Do not mutate the state or the component will not reflect the changes when it is connected.
Then we connect the container component using react-redux which applies state and actions to props. So now any time the users state changes it will send it to the container component and down to the UserList component for rendering.
This is a naive approach but I believe it illustrates the solution and gets you on the right track!
Good luck and hope this helped!

Create an instance of a React class from a string

I have a string which contains a name of the Class (this is coming from a json file). This string tells my Template Class which layout / template to use for the data (also in json). The issue is my layout is not displaying.
Home.jsx:
//a template or layout.
var Home = React.createClass({
render () {
return (
<div>Home layout</div>
)
}
});
Template.jsx:
var Template = React.createClass({
render: function() {
var Tag = this.props.template; //this is the name of the class eg. 'Home'
return (
<Tag />
);
}
});
I don't get any errors but I also don't see the layout / Home Class. I've checked the props.template and this logs the correct info. Also, I can see the home element in the DOM. However it looks like this:
<div id='template-holder>
<home></home>
</div>
If I change following line to:
var Tag = Home;
//this works but it's not dynamic!
Any ideas, how I can fix this? I'm sure it's either simple fix or I'm doing something stupid. Help would be appreciated. Apologies if this has already been asked (I couldn't find it).
Thanks,
Ewan
This will not work:
var Home = React.createClass({ ... });
var Component = "Home";
React.render(<Component />, ...);
However, this will:
var Home = React.createClass({ ... });
var Component = Home;
React.render(<Component />, ...);
So you simply need to find a way to map between the string "Home" and the component class Home. A simple object will work as a basic registry, and you can build from there if you need more features.
var components = {
"Home": Home,
"Other": OtherComponent
};
var Component = components[this.props.template];
No need to manually map your classes to a dictionary, or "registry", as in Michelle's answer. A wildcard import statement is already a dictionary!
import * as widgets from 'widgets';
const Type = widgets[this.props.template];
...
<Type />
You can make it work with multiple modules by merging all the dictionaries into one:
import * as widgets from 'widgets';
import * as widgets2 from 'widgets2';
const registry = Object.assign({}, widgets, widgets2);
const widget = registry[this.props.template];
I would totally do this to get dynamic dispatch of react components. In fact I think I am in a bunch of projects.
I had the same problem, and found out the solution by myself. I don't know if is the "best pratice" but it works and I'm using it currently in my solution.
You can simply make use of the "evil" eval function to dynamically create an instance of a react component. Something like:
function createComponent(componentName, props, children){
var component = React.createElement(eval(componentName), props, children);
return component;
}
Then, just call it where you want:
var homeComponent = createComponent('Home', [props], [...children]);
If it fits your needs, maybe you can consider something like this.
Hope it helps.
I wanted to know how to create React classes dynamically from a JSON spec loaded from a database and so I did some experimenting and figured it out. My basic idea was that I wanted to define a React app through a GUI instead of typing in code in a text editor.
This is compatible with React 16.3.2. Note React.createClass has been moved into its own module.
Here's condensed version of the essential parts:
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import createReactClass from 'create-react-class'
const spec = {
// getDefaultProps
// getInitialState
// propTypes: { ... }
render () {
return React.createElement('div', null, 'Some text to render')
}
}
const component = createReactClass(spec)
const factory = React.createFactory(component)
const instance = factory({ /* props */ })
const str = ReactDOMServer.renderToStaticMarkup(instance)
console.log(str)
You can see a more complete example here:
https://github.com/brennancheung/02-dynamic-react/blob/master/src/commands/tests/createClass.test.js
Here is the way it will work from a string content without embedding your components as statically linked code into your package, as others have suggested.
import React from 'react';
import { Button } from 'semantic-ui-react';
import createReactClass from 'create-react-class';
export default class Demo extends React.Component {
render() {
const s = "return { render() { return rce('div', null, rce(components['Button'], {content: this.props.propA}), rce(components['Button'], {content: 'hardcoded content'})); } }"
const createComponentSpec = new Function("rce", "components", s);
const componentSpec = createComponentSpec(React.createElement, { "Button": Button });
const component = React.createElement(createReactClass(componentSpec), { propA: "content from property" }, null);
return (
<div>
{component}
</div>
)
}
}
The React class specification is in string s. Note the following:
rce stands for React.createElement and given as a first param when callingcreateComponentSpec.
components is a dictionary of extra component types and given as a second param when callingcreateComponentSpec. This is done so that you can provide components with clashing names.
For example string Button can be resolved to standard HTML button, or button from Semantic UI.
You can easily generate content for s by using https://babeljs.io as described in https://reactjs.org/docs/react-without-jsx.html. Essentially, the string can't contain JSX stuff, and has to be plain JavaScript. That's what BabelJS is doing by translating JSX into JavaScript.
All you need to do is replace React.createElement with rce, and resolve external components via components dictionary (if you don't use external components, that you can skip the dictionary stuff).
Here is equivalent what in the code above. The same <div> with two Semantic UI Buttons in it.
JSX render() code:
function render() {
return (
<div>
<Button content={this.props.propA}/>
<Button content='hardcoded content'/>
</div>
);
}
BabelJS translates it into:
function render() {
return React.createElement("div", null, React.createElement(Button, {
content: this.props.propA
}), React.createElement(Button, {
content: "hardcoded content"
}));
}
And you do replacement as outlined above:
render() { return rce('div', null, rce(components['Button'], {content: this.props.propA}), rce(components['Button'], {content: 'hardcoded content'})); }
Calling createComponentSpec function will create a spec for React class.
Which then converted into actual React class with createReactClass.
And then brought to life with React.createElement.
All you need to do is return it from main component render func.
When you use JSX you can either render HTML tags (strings) or React components (classes).
When you do var Tag = Home, it works because the JSX compiler transforms it to:
var Template = React.createElement(Tag, {});
with the variable Tag in the same scope and being a React class.
var Tag = Home = React.createClass({
render () {
return (
<div>Home layout</div>
)
}
});
When you do
var Tag = this.props.template; // example: Tag = "aClassName"
you are doing
var Template = React.createElement("aClassName", null);
But "aClassName" is not a valid HTML tag.
Look here