React - easy solution for global state or antipattern? - reactjs

In my React app I have main component that contians the whole state of the application and also functions used to modify the state.
In order to get access to the state and the functions in all subcomponents I'm passing this from the main component through subcomponent attributes (app):
class App extends React.Component {
state = DefaultState;
funcs = getController(this);
render() {
return (
<Header app={this} />
<Page app={this} />
<Footer app={this} />
)
}
}
function getController(app: App) {
return {
switchPage: (page: string) => {
app.setState({ page: page });
},
}
}
Therefore in subcomponents I can access the state variables and modify them like this:
const HeaderComponent: React.FC<CompProps> = (props) => {
return (
<div>
<h1>{props.app.state.currentPage}</h1>
<button onClick={(e) => {props.app.funcs.switchPage('home')}}>Home</button>
</div>
)
}
This is the quickest/simplest solution I've found for global state. However I've never seen this in any tutorial or so. I guess main problem is the whole app will rerender when a single value is changed in global state, but the same goes for React Context API.
Question is that are there any disadvantages of this approach or a reason to not use this?

You can save func to an exported let variable and utilize the most recent version of func without re-rendering, but as this isn't a common occurrence, you won't find much information about it. Since it's simply javascript, any known hack will work. Also, the part of your question regarding react-context re-rendering is correct although you must consider that it will re-render and it will be more pruned for optimization of unmodified siblings.
You may alternatively supply a simple ref (useRef) to those components, which will allow them to access the most recent version of func, but because the ref reference itself does not change when the page is re-rendered, they will not be updated for function change.
I'm using react functional component but the class base may be so similar
export let funcs = null
const App = () => {
funcs = getController();
render() {
return (
<Header />
<Page />
<Footer />
)
}
}
// header component
import { funcs as appFuncs } from '~/app.js'
const HeaderComponent: React.FC<CompProps> = (props) => {
return (
<div>
{/* same thing can be happened for the state */}
<button onClick={(e) => {appFuncs.switchPage('home')}}>Home</button>
</div>
)
}
Hooks version
const App = () => {
const ref = useRef();
funcs = getController();
ref.current = {state, funcs};
// note ref.current changes not the ref itself
render() {
return (
<Header app={ref} />
<Page app={ref} />
<Footer app={ref} />
)
}
}
// header
const HeaderComponent: React.FC<CompProps> = (props) => {
return (
<div>
<h1>{props.app.current.state.currentPage}</h1>
<button onClick={(e) => {props.app.current.func.switchPage('home')}}>Home</button>
</div>
)
}
Any suggestions or other techniques will be much appreciated.

Related

React Context, how to update context in one branch, to use in another branch

Can I use the React Context API to manage the global state for itemsInCart, allowing Product to update the context, and only Nav to use and re-render. I don't want to re-render the whole app on change.
Reading the React docs, I see Context users need to be downstream from the providers, so not sure this will be possible
Code (dramatically simplified):
function NavBar () {
const [itemsInCart, setItemsInCart] = useState(0)
return (
<button onClick={openCart}>{itemsInCart}</button>
)
}
function Product() {
function incrementCart () {
???
}
return (
<button onClick={() => incrementCart}>add product to cart</button>
)
}
function App () {
return (
<NavBar/>
<Router>
<Product/>
</Router>
)
}
You want to use useContext to do it.
A component calling useContext will always re-render when the context
value changes. If re-rendering the component is expensive, you can
optimize it by using memoization.
For example:
export const CartContext= React.createContext();
export const CartProvider = ({ children }) => {
const [itemsInCart, setItemsInCart] = useState(null);
return (
<CartContext.Provider
value={
itemsInCart
}
>
{children}
</CartContext.Provider>
);
};
Then in some function you can use to get an update the cart:
const {itemsInCart, setItemsInCart} = useContext(CartContext);
And lastly, update the Provider wrapping like:
function App () {
return (
<CartProvider>
<NavBar/>
<Router>
<Product/>
</Router>
</CartProvider>
)
}

react recreating a component when I don't want to

I'm super new to react, this is probably a terrible question but I'm unable to google the answer correctly.
I have a component (CogSelector) that renders the following
import React from "react"
import PropTypes from "prop-types"
import Collapsible from 'react-collapsible'
import Cog from './cog.js'
const autoBind = require("auto-bind")
import isResultOk from "./is-result-ok.js"
class CogSelector extends React.Component {
constructor(props) {
super(props)
this.state = {
docs: null,
loaded: false,
error: null
}
autoBind(this)
}
static get propTypes() {
return {
selectCog: PropTypes.func
}
}
shouldComponentUpdate(nextProps, nextState){
if (nextState.loaded === this.state.loaded){
return false;
} else {
return true;
}
}
componentDidMount() {
fetch("/api/docs")
.then(isResultOk)
.then(res => res.json())
.then(res => {
this.setState({docs: res.docs, loaded: true})
}, error => {
this.setState({loaded: true, error: JSON.parse(error.message)})
})
}
render() {
const { docs, loaded, error } = this.state
const { selectCog } = this.props
if(!loaded) {
return (
<div>Loading. Please wait...</div>
)
}
if(error) {
console.log(error)
return (
<div>Something broke</div>
)
}
return (
<>
Cogs:
<ul>
{docs.map((cog,index) => {
return (
<li key={index}>
<Cog name={cog.name} documentation={cog.documentation} commands={cog.commands} selectDoc={selectCog} onTriggerOpening={() => selectCog(cog)}></Cog>
</li>
// <li><Collapsible onTriggerOpening={() => selectCog(cog)} onTriggerClosing={() => selectCog(null)} trigger={cog.name}>
// {cog.documentation}
// </Collapsible>
// </li>
)
})}
{/* {docs.map((cog, index) => { */}
{/* return ( */}
{/* <li key={index}><a onClick={() => selectCog(cog)}>{cog.name}</a></li>
)
// })} */}
</ul>
</>
)
}
}
export default CogSelector
the collapsible begins to open on clicking, then it calls the selectCog function which tells it's parent that a cog has been selected, which causes the parent to rerender which causes the following code to run
class DocumentDisplayer extends React.Component{
constructor(props) {
super(props)
this.state = {
cog: null
}
autoBind(this)
}
selectCog(cog) {
this.setState({cog})
}
render(){
const { cog } = this.state
const cogSelector = (
<CogSelector selectCog={this.selectCog}/>
)
if(!cog) {
return cogSelector
}
return (
<>
<div>
{cogSelector}
</div>
<div>
{cog.name} Documentation
</div>
<div
dangerouslySetInnerHTML={{__html: cog.documentation}}>
</div>
</>
)
}
}
export default DocumentDisplayer
hence the cogSelector is rerendered, and it is no longer collapsed. I can then click it again, and it properly opens because selectCog doesn't cause a rerender.
I'm pretty sure this is just some horrible design flaw, but I would like my parent component to rerender without having to rerender the cogSelector. especially because they don't take any state from the parent. Can someone point me to a tutorial or documentation that explains this type of thing?
Assuming that Collapsible is a stateful component that is open by default I guess that the problem is that you use your component as a variable instead of converting it into an actual component ({cogSelector} instead of <CogSelector />).
The problem with this approach is that it inevitably leads to Collapsible 's inner state loss because React has absolutely no way to know that cogSelector from the previous render is the same as cogSelector of the current render (actually React is unaware of cogSelector variable existence, and if this variable is re-declared on each render, React sees its output as a bunch of brand new components on each render).
Solution: convert cogSelector to a proper separated component & use it as <CogSelector />.
I've recently published an article that goes into details of this topic.
UPD:
After you expanded code snippets I noticed that another problem is coming from the fact that you use cogSelector 2 times in your code which yields 2 independent CogSelector components. Each of these 2 is reset when parent state is updated.
I believe, the best thing you can do (and what you implicitly try to do) is to lift the state up and let the parent component have full control over all aspects of the state.
I solved this using contexts. Not sure if this is good practice but it certainly worked
render() {
return (
<DocContext.Provider value={this.state}>{
<>
<div>
<CogSelector />
</div>
{/*here is where we consume the doc which is set by other consumers using updateDoc */}
<DocContext.Consumer>{({ doc }) => (
<>
<div>
Documentation for {doc.name}
</div>
<pre>
{doc.documentation}
</pre>
</>
)}
</DocContext.Consumer>
</>
}
</DocContext.Provider>
)
}
then inside the CogSelector you have something like this
render() {
const { name, commands } = this.props
const cog = this.props
return (
//We want to update the context object by using the updateDoc function of the context any time the documentation changes
<DocContext.Consumer>
{({ updateDoc }) => (
<Collapsible
trigger={name}
onTriggerOpening={() => updateDoc(cog)}
onTriggerClosing={() => updateDoc(defaultDoc)}>
Commands:
<ul>
{commands.map((command, index) => {
return (
<li key={index}>
<Command {...command} />
</li>
)
}
)}
</ul>
</Collapsible>
)}
</DocContext.Consumer>
)
}
in this case it causes doc to be set to what cog was which is a thing that has a name and documentation, which gets displayed. All of this without ever causing the CogSelector to be rerendered.
As per the reconciliation algorithm described here https://reactjs.org/docs/reconciliation.html.
In your parent you have first rendered <CogSelector .../> but later when the state is changed it wants to render <div> <CogSelector .../></div>... which is a completely new tree so react will create a new CogSelector the second time

Creating a parent 'workspace' component in ReactJS

Using ReactJS, I am trying to create a common workspace component that will have toolbar buttons and a navigation menu. The idea I have is to re-use this component to wrap all other dynamic components that I render in the app.
Currently, I've created a Toolbar and MenuBar components that I then add to each component in the app as such:
<Toolbar/>
<MenuBar/>
<Vendors/>
This does not feel right, since my aim is to have just one component which would be something like:
<Workspace>
<Vendor/>
</Workspace>
However, I am not sure of how to achieve this and whether this is the right approach.
As to whether or not it is the right approach is subjective, but I can provide insight into one way to make a "wrapper" type component:
// Your workspace wrapper component
class Workspace {
render() {
return (
<div className="workspace">
<div className="workspace__toolbar">
Toolbar goes here
</div>
<div className="workspace__nav">
Navgoes here
</div>
<div className="workspace__content">
{this.props.children}
</div>
</div>
)
}
}
// Using the component to define another one
class MyComponent {
render() {
return (
<Workspace>
This is my workspace content.
</Workspace>
)
}
}
You can also look at HOC's or Higher Order Components to wrap things.
React offer two traditional ways to make your component re useable
1- High-order Components
you can separate the logic in withWorkspace and then give it a component to apply that logic into it.
function withWorkSpace(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
const Component = () => {
const Content = withWorkSpace(<SomeOtherComponent />)
return <Content />
}
2- Render Props
or you can use function props then give the parent state as arguments, just in case you need the parent state in child component.
const Workspace = () => {
state = {}
render() {
return (
<div className="workspace">
<div className="workspace__toolbar">
{this.props.renderTollbar(this.state)}
</div>
<div className="workspace__nav">
{this.props.renderNavigation(this.state)}
</div>
<div className="workspace__content">
{this.props.children(this.state)}
</div>
</div>
)
}
}
const Toolbar = (props) => {
return <div>Toolbar</div>
}
const Navigation = (props) => {
return <div>Toolbar</div>
}
class Component = () => {
return (
<Workspace
renderNavigation={(WorkspaceState) => <Navigation WorkspaceState={WorkspaceState} />}
renderTollbar={(WorkspaceState) => <Toolbar {...WorkspaceState} />}
>
{(WorkspaceState) => <SomeComponentForContent />}
</Workspace>
)
}

ReactJS: Nothing was returned from render while testing HOC component

I have the following code:
TableWrapper.jsx
const TableWrapper = props => {
return (
<div>
{props.table}
</div>
);
}
Then it's being used in Foo.jsx
export class FooTable extends React.Component {
render() {
return (
<div>
<TableWrapper
table={<ListTable />}
/>
</div>
);
}
}
Here is ListTable.jsx
render() {
const {data, error, asyncStatus} = this.props.instanceList;
return (
<div>
<CustomTable
title='123'
/>
</div>
)
}
I use jest and enzyme and for one of the tests:
it('Simulate search input field', () => {
const container = mount(<FooTable {...mockProps} />);
});
});
I am getting
console.error node_modules/jsdom/lib/jsdom/virtual-console.js:29
Error: Uncaught [Invariant Violation: TableWrapperComponent(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.]
If I use shallow I get no error. What am I missing?
You should use render and then put your component inside your render prop.
<TableWrapper render={table => <ListTable props={...this.props} />} />
Now I will ask where is your CustomTable that you are using in ListTable? At some point you will have to make a your initial component or use one from the many libraries out there.
const myGenericComponent = (props) => {
// create variables deconstruct props as you like
const name = props.name
return (
// assuming I passed in name from my parent component as one of my props
<div><table><th>Table {name}</th><td>{name}</td></table>
)
}
export default myGenericComponent;

React - pass object from Container Component to Presentational Component

I'm trying to dynamically add Components (based on ID from an array) into my Presentational Component. I'm new to all this so there is a possibility I'm making it way too difficult for myself.
Here's the code of my Container Component:
class TemplateContentContainer extends Component {
constructor() {
super()
this.fetchModule = this.fetchModule.bind(this)
this.removeModule = this.removeModule.bind(this)
this.renderModule = this.renderModule.bind(this)
}
componentWillReceiveProps(nextProps) {
if(nextProps.addAgain !== this.props.addAgain) // prevent infinite loop
this.fetchModule(nextProps.addedModule)
}
fetchModule(id) {
this.props.dispatch(actions.receiveModule(id))
}
renderModule(moduleId) {
let AddModule = "Modules.module" + moduleId
return <AddModule/>
}
removeModule(moduleRemoved) {
console.log('remove clicked' + moduleRemoved)
this.props.dispatch(actions.removeModule(moduleRemoved))
}
render() {
return (
<div>
<TemplateContent
addedModule={this.props.addedModule}
templateModules={this.props.templateModules}
removeModule={this.removeModule}
renderModule={this.renderModule}
/>
</div>
)
}
}
and the code of the Presentational Component:
const TemplateContent = (props) => {
let templateModules = props.templateModules.map((module, index) => (
<li key={index}>
{props.renderModule(module)}
<button onClick={props.removeModule.bind(this, index)}>
remove
</button>
</li>
))
return (
<div>
<ul>
{templateModules}
</ul>
</div>
)
}
the renderModule function returns object, but when it's being passed to the presentational Component it doesn't work anymore (unless it's passed as className for example then it returns object)
I'm importing the modules from modules folder where I export them all into index.js file
import * as Modules from '../components/modules'
Hope it makes sense, any help would be highly appreciated.
Thanks a lot in advance!
I would recommend to restructure the files to make for an easier handling.
If your container components render would look like this:
render () {
return (
<div>
<ul>
{this.props.templateModules.map(module => (
<ChildComponent onRemove={this.removeModule} module={module} />
)}
</ul>
</div>
)
}
Your child component can just handle the remove click and the displaying of the module data
EDIT:
My bad, I just misunderstood your problem.
I would map the ids to the according components instead of concatenating the name of the Component you want to render, so your container component would look something like this:
getChildComponent (id) {
const foo = {
foo: () => {
return <Foo onRemove={this.removeModule} />
},
bar: () => {
return <Bar onRemove={this.removeModule} />
}
}
return foo[id]
}
render () {
return (
<div>
<ul>
{this.props.templateModules.map(module => (
{this.getChildComponent(module.id)()}
)}
</ul>
</div>
)
}
Also you should maybe have a look at react-redux and move your dispatches to react-redux containers.

Resources