Having a bit of a hard time finding anything about this specific pattern, and I'm not even exactly sure how to describe it in search terms so apologies if this is a duplicate question (although I don't think it is).
I want to output a React layout based on an order passed to the app that the user can set via a settings panel. The idea is that there are a few different containers to output on the page that I want the user to be able to re-arrange. It's important to note that this order is not changeable after the app renders. (I want the user to be able to say "Show me PanelA, PanelC and PanelB in that order")
Now, I've figured out how to accomplish this using the following pattern:
// user-ordered array is passed to the app:
const settings = {
layout: [
"panela",
"panelb",
"panelc"
]
}
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
// some state
}
}
renderComponentsInOrder() {
return settings.layout.map(component => {
switch(component) {
case "panela":
return
<PanelA {.../* some state */} />
case "panelb":
return
<PanelB {.../* some state */} />
case "panelc":
return
<PanelC {.../* some state */} />
default: return null
}
})
}
render() {
return this.renderComponentsInOrder()
}
}
but this strikes me as really inefficient. The order of the components shouldn't need to be re-calculated every time render runs because the order won't change while the app is running. I've tried doing things like memoizing the renderComponentsInOrder method to cache the calculated layout or renaming the values in the settings.layout array and calling them directly, but haven't been able to get anything to work because the components need to update based on state.
Any help or advice would be greatly appreciated. Thanks!
EDIT: Ideally I'm looking for a JS-based solution as compatibility is a bit of an issue and I don't want to rely solely on the browser's implementation of CSS.
A slightly different take on the answer given by Brandon. You could have a function to generate the component based on the state/props:
const settings = {
layout: [
"panela",
"panelb",
"panelc"
]
};
const panels = settings.layout.map(c => {
switch (c) {
case "panela": return (props, state) => <PanelA key="a" foo={state.foo} />
case "panelb": return (props, state) => <PanelB key="b" bar={state.bar} />
}
});
// Now use panels array to render:
class MyComponent extends React.Component {
render() {
const props = this.props;
const state = this.state;
const ordered = panels.map(p => p(props, state));
return <div>{ordered}</div>
}
}
Method 1:
Just transform the settings array into an array of Components once before you render:
const settings = {
layout: [
"panela",
"panelb",
"panelc"
]
};
const panels = settings.layout.map(c => {
switch (c) {
case "panela": return { Component: PanelA, key: c };
case "panelb": return { Component: PanelA, key: c };
...
}
});
// Now use panels array to render:
class MyComponent extends React.Component {
// ...
renderComponentsInOrder() {
return panels.map(({Component, key}) => (
<Component key={key} {.../* some state*/} />
));
}
// ...
}
Method 2:
Just create a mapping table:
const settings = {
layout: [
"panela",
"panelb",
"panelc"
]
};
const panelLookup = {
"panela": PanelA,
"panelb": PanelB,
...
};
// Now use lookup to render:
// Now use panels array to render:
class MyComponent extends React.Component {
// ...
renderComponentsInOrder() {
return settings.layout.map(key => {
const Component = panelLookup[key];
return <Component key={key} {.../* some state*/} />;
});
}
// ...
}
Related
I'm having serious issues with the "new" React Context ( https://reactjs.org/docs/context.html ) to work like I want/expect from the documentation. I'm using React v.16.8.6 (upgrading will probably take ages, it's a big app). I know there is a bit of a mix between old and new stuff but plz don't get stuck on that..
I did it like this to be as flexible as possible but it doesn't work.
The issue is, when it comes to contextAddToCart(..) it only executes the empty function instead of the one I defined in state as the documentation this.addToCart. I have consumers in other places as well. It seems like perhaps it's executing this in the wrong order. Or every time a Compontent imports MinicartContext it's reset to empty fn.. I don't know how to get around this..
I'll just post the relevant code I think will explain it best:
webpack.config.js:
const APP_DIR = path.resolve(__dirname, 'src/');
module.exports = function config(env, argv = {}) {
return {
resolve: {
extensions: ['.js', '.jsx'],
modules: [
path.resolve(__dirname, 'src/'),
'node_modules',
],
alias: {
contexts: path.resolve(__dirname, './src/contexts.js'),
},
contexts.js
import React from 'react';
export const MinicartContext = React.createContext({
addToCart: () => {},
getState: () => {},
});
MinicartContainer.jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
MinicartContext,
} from 'contexts';
export default class MinicartContainer extends Component {
constructor(props) {
super(props);
this.addToCart = (product, qty) => {
const { prices } = product;
const { grandTotal, qtyTotal } = this.state;
this.setState({
grandTotal: grandTotal + prices.price,
qtyTotal: qtyTotal + qty,
});
};
this.state = {
grandTotal: -1,
qtyTotal: -1,
currencyCode: '',
addToCart: this.addToCart,
};
}
render() {
const { children } = this.props;
return (
<MinicartContext.Provider value={this.state}>
{children}
</MinicartContext.Provider>
);
}
Header.jsx:
import React, { Component } from 'react';
import {
MinicartContext,
} from 'contexts';
class Header extends Component {
render() {
return (
<div>
<MinicartContainer MinicartContext={MinicartContext}>
<Minicart MinicartContext={MinicartContext} />
</MinicartContainer MinicartContext={MinicartContext}>
{/* stuff */}
<MinicartContainer MinicartContext={MinicartContext}>
<Minicart MinicartContext={MinicartContext} />
</MinicartContainer MinicartContext={MinicartContext}>
</div>
)
}
}
export default Header;
AddToCartButton.jsx
import {
MinicartContext,
} from 'contexts';
export default class AddToCartButton extends Component {
addToCart(e, contextAddToCart) {
e.preventDefault();
const QTY = 1;
const { product, active } = this.props;
// doing stuff ...
contextAddToCart(product, QTY);
}
render() {
return (
<React.Fragment>
<MinicartContext.Consumer>
{({context, addToCart}) => (
<div
onClick={(e) => { this.addToCart(e, addToCart); }}
Seems to me that you don't have fully understand how the context API words.
Here's my HOC implementation of contexts, maybe it can help you to understand better how things work.
export const MinicartContext = React.createContext({}) // Export the Context so we can use the Consumer in class and functional components (above). Don't use the Provider from here.
// Wrap the provider to add some custom values.
export const MinicartProvider = props => {
const addToCart = () => {
//Add a default version here
};
const getState = () => {
//Add a default version here
};
// Get the custom values and override with instance ones.
const value = {addToCart, getState, ...props.value}
return <MinicartContext.Provider value={value}>
{props.children}
</MinicartContext.Provider>
}
Then when using the provider:
const SomeComponent = props => {
const addToCart = () => {
//A custom version used only in this component, that need to override the default one
};
//Use the Wrapper, forget the MinicartContext.Provider
return <MinicartProvider value={{addToCart}}>
/* Stuff */
</MinicartProvider>
}
And when using the consumer you have three options:
Class Components with single context
export default class AddToCartButton extends Component {
static contextType = MinicartContext;
render (){
const {addToCart, getState} = this.context;
return (/*Something*/)
}
}
Class Components with multiple contexts
export default class AddToCartButton extends Component {
render (){
return (
<MinicartContext.Consumer>{value => {
const {addToCart, getState} = value
return (/*Something*/)
}}</MinicartContext.Consumer>
)
}
}
Functional Components
const AddToCartButton = props => {
const {addToCart, getState} = useContext(MinicartContext);
}
You can create the Wrapper Provider as a class component too, and pass the full state as value, but it's unnecessary complexity.
I Recommend you take a look at this guide about contexts, and also, avoid using the same name on the same scope... Your AddToCartButton.jsx file was reeeeally confusing :P
The issue I had was that I was using <MinicartContainer> in multiple places but all should act as one and the same. Changing it so it wrapped all elements made other elements reset their state when the context updated.
So the only solution I found was to make everything static (including state) inside MinicartContainer, and keep track of all the instances and then use forceUpdate() on all (needed) instances. (Since I am never doing this.setState nothing ever updates otherwise)
I though the new React context would be a clean replacement for things like Redux but as it stands today it's more a really vague specification which can replace Redux in a (sometimes) non standard way.
If you can just wrap all child Consumers with a single Provider component without any side-effects then you can make it a more clean implementation. That said I don't think what I have done is bad in any way but not what people expect a clean implementation should look like. Also this approach isn't mentioned in the docs at all either.
In addition to Toug's answer, I would memoize the exposed value prop of the provider. Otherwise it will re-render it's subscribers every time even if the state doesn't change.
export const MinicartContext = React.createContext({}) // Export the Context so we can use the Consumer in class and functional components (above). Don't use the Provider from here.
// Wrap the provider to add some custom values.
export const MinicartProvider = props => {
const addToCart = () => {
//Add a default version here
};
const getState = () => {
//Add a default version here
};
// Get the custom values and override with instance ones.
const value = useMemo(
() => ({addToCart, getState, ...props.value}),
[addToCart, getState, props.value]
);
return <MinicartContext.Provider value={value}>
{props.children}
</MinicartContext.Provider>
}
How do I filter data in a component based on a parent prop?
My data to compare to contains something like:
{
"id": "5a7847508f9337cf77712128",
"index": 0,
"projectName": "Ovolo",
"location": "Stoddard Place, Riverton, New York, 3571"
},
{
"id": "5a7847503101a8ef7f7d3c30",
"index": 1,
"projectName": "Isonus",
"location": "Elliott Walk, Magnolia, Minnesota, 4488"
}...
My component looks something like this:
import React, {Component} from 'react';
import data from "../data/projects.json";
const ProjectTitle = data.filter((projects, props) => {
if (projects.id === props.project_id )
var title = projects.projectName;
return title;
});
class ProjectName extends Component {
constructor() {
super();
this.state = {
projectTitle: ProjectTitle
}
}
componentDidMount() {
this.setState({
projectTitle: ProjectTitle
});
}
render() {
return (
<h4 className="projectName">
{ this.state.projectTitle }
</h4>
);
}
}
export default ProjectName;
Basically, I want to match props.project_id from the parent component with the first project from my data that matches the id, so I can display the projectName using the component. Currently, the one that I tried does not return the name of the project. I am new to this so I need help and guidance, maybe an example or a reference may help.
Basically you wants to watch for different project_id sent from the parent and depending on which want to filter the data, as far as i understood.
One thing you can do is use componentWillReceiveProps lifecycle to watch for new props. and can also componentDidMount lifecycle to set the projecttitle.
you need to change the projectTitle function also
const ProjectTitle = (projectId) => {
const project = data.filter((projects) => {
if (projects.id === projectId ) {
return true;
}
return false
});
if(project) {
return projects.projectName;
}
return project
}
componentWillReceiveProps(nextProps) {
const {project_id} = nextProps
const newProjectTitle = ProjectTitle(project_id)
this.setState({
projectTitle: newProjectTitle,
})
}
componentDidMount() {
const {project_id} = this.props
const newProjectTitle = ProjectTitle(project_id)
this.setState({
projectTitle: newProjectTitle,
})
}
By adding componentWIllReceiveprops and componentDidMount you can
dynamically change the project name whenever new project_id is passed
as a props. you can also use the constructor function to initialize the state like this
constructor(props) {
super(props)
this.state = {
projectTitle: ProjectTitle(props.project_id),
}
}
Refer this lifecycle methods in react for more reference
Not sure why it isn't working exactly but that ProjectTitle that you create outside the class doesn't ring well with me. I think that you're assigning it to the state and that's not working as expected. You should do the filter (or find) operation in the componentWillMount function of the React lifecycle . After you find the project you do the setState and it should work.
class ProjectName extends Component {
constructor() {
super();
this.state = {
projectTitle: ''
}
}
componentWillMount() {
const project = data.find(project => project.id === this.props.project_id);
if (project) {
this.setState({
projectTitle: project.projectName
});
}
}
render() {
return (
<h4 className="projectName">
{this.state.projectTitle}
</h4>
);
}
}
export default ProjectName;
Assuming props.project_id is a ProjectName's prop the easiest solution is to move the filter to the render function. Also, if you want to find one record use find not filter.
render() {
const project = data.find(( {id} ) => id === this.props.project_id)
return (
<h4 className="projectName">
{ project && project.title }
</h4>
);
}
You could do as other suggested and set the state during the component's creation, but then you'd have to do the same in componentWillReceiveProps, this creates a lot of unnecessary complexity.
If performance is an issue (it generally isn't) and you want to find the project only once per id, use a memoized function and keep the logic in the render function. This is a lot simpler and has the same effect.
I'm trying to perform authorization on a child component against certain permissions. I'm using ref callback to get access to the Node, wherein I can check permissions. Depending on these permissions, I would like to unmount the component.
Within the callback, I'm trying to use ReactDOM.findDOMNode() and then ReactDOM.unmountComponentAtNode() to remove it. The latter keeps returning false, although findDomNode appears to properly be selecting the DOM element.
class Auth extends React.Component {
...
checkPermissions(component) {
const domNode = ReactDOM.findDOMNode(component); // => <p>...</p>
if (domNode) {
let wasUnmounted = ReactDOM.unmountComponentAtNode(domNode);
console.log('was unmounted', wasUnmounted); // => false
}
}
render(){
return (
<div>
{this.state.authorized &&
<Component ref={(c) => this.checkPermissions(c)} {...this.props} />
}
</div>
)
}
...
How can I use ReactDOM.unmountComponentAtNode() to effectively remove my component?
I don't think you'll want to mount your node just to check permissions and then unmount it. You should check permissions before you render. Not only is it more secure, but it's also simpler.
If the user is authorized, you render the component. If the user is not authorized, you render something else.
So something kind of like this:
render() {
if (!this.state.authorized) {
return <PleaseLogIn />;
}
return (
<div>
<Component {...this.props} />
</div>
);
}
If you find yourself manipulating the DOM manually, take a step back and make sure there's not a more "Reacty" way to do it.
Update:
If you want a wrapper component that you can put around things that should or shouldn't render its children based on permissions maybe you do something like this:
// state.userPermissions = ['permission1', 'permission1', 'betaTolerant'];
const AuthWrapper = React.createClass({
propTypes: {
userPermissions: React.PropTypes.array.isRequired,
requiredPermissions: React.PropTypes.array.isRequired,
children: React.PropTypes.node.isRequired
},
isAllowed() {
const { userPermissions, requiredPermissions } = this.props;
return requiredPermissions.some((requiredPermission) => {
return userPermissions.some((userPermission) => {
// If this ever returns true, isAllowed will return true
// Meaning: If any of the requiredPermissions match
// any of the userPermissions
return requiredPermission === userPermission;
});
});
},
render {
if(!this.isAllowed()) return null;
return this.props.children;
};
});
const mapStateToProps = (state) => {
// Only this auth component has access to state
return {
userPermissions: state.userPermissions
};
};
export default connect(
mapStateToProps,
null
)(AuthWrapper);
Now you can use this wrapper like:
// Inside some component
render {
return (
<MyApp>
<NormalFeature />
<AuthWrapper requiredPermissions=['secretFeature', 'betaTolerant']>
<SecretFeature />
</AuthWrapper>
</MyApp>
);
}
I'm new to React, and I'd like to ask a strategy question about how best to accomplish a task where data must be communicated between sibling components.
First, I'll describe the task:
Say I have multiple <select> components that are children of a single parent that passes down the select boxes dynamically, composed from an array. Each box has exactly the same available options in its initial state, but once a user selects a particular option in one box, it must be disabled as an option in all other boxes until it is released.
Here's an example of the same in (silly) code. (I'm using react-select as a shorthand for creating the select boxes.)
In this example, I need to disable (ie, set disabled: true) the options for "It's my favorite" and "It's my least favorite" when a user selects them in one select box (and release them if a user de-selects them).
var React = require('react');
var Select = require('react-select');
var AnForm = React.createClass({
render: function(){
// this.props.fruits is an array passed in that looks like:
// ['apples', 'bananas', 'cherries','watermelon','oranges']
var selects = this.props.fruits.map(function(fruit, i) {
var options = [
{ value: 'first', label: 'It\'s my favorite', disabled: false },
{ value: 'second', label: 'I\'m OK with it', disabled: false },
{ value: 'third', label: 'It\'s my least favorite', disabled: false }
];
return (
<Child fruit={fruit} key={i} options={options} />
);
});
return (
<div id="myFormThingy">
{fruitSelects}
</div>
)
}
});
var AnChild = React.createClass({
getInitialState: function() {
return {
value:'',
options: this.props.options
};
},
render: function(){
function changeValue(value){
this.setState({value:value});
}
return (
<label for={this.props.fruit}>{this.props.fruit}</label>
<Select
name={this.props.fruit}
value={this.state.value}
options={this.state.options}
onChange={changeValue.bind(this)}
placeholder="Choose one"
/>
)
}
});
Is updating the child options best accomplished by passing data back up to the parent through a callback? Should I use refs to access the child components in that callback? Does a redux reducer help?
I apologize for the general nature of the question, but I'm not finding a lot of direction on how to deal with these sibling-to-sibling component interactions in a unidirectional way.
Thanks for any help.
TLDR: Yes, you should use a props-from-top-to-bottom and change-handlers-from-bottom-to-top approach. But this can get unwieldy in a larger application, so you can use design patterns like Flux or Redux to reduce your complexity.
Simple React approach
React components receive their "inputs" as props; and they communicate their "output" by calling functions that were passed to them as props. A canonical example:
<input value={value} onChange={changeHandler}>
You pass the initial value in one prop; and a change handler in another prop.
Who can pass values and change handlers to a component? Only their parent. (Well, there is an exception: you can use the context to share information between components, but that's a more advanced concept, and will be leveraged in the next example.)
So, in any case, it's the parent component of your selects that should manage the input for your selects. Here is an example:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
// keep track of what is selected in each select
selected: [ null, null, null ]
};
}
changeValue(index, value) {
// update selected option
this.setState({ selected: this.state.selected.map((v, i) => i === index ? value : v)})
}
getOptionList(index) {
// return a list of options, with anything selected in the other controls disabled
return this.props.options.map(({value, label}) => {
const selectedIndex = this.state.selected.indexOf(value);
const disabled = selectedIndex >= 0 && selectedIndex !== index;
return {value, label, disabled};
});
}
render() {
return (<div>
<Select value={this.state.selected[0]} options={this.getOptionList(0)} onChange={v => this.changeValue(0, v)} />
<Select value={this.state.selected[1]} options={this.getOptionList(1)} onChange={v => this.changeValue(1, v)} />
<Select value={this.state.selected[2]} options={this.getOptionList(2)} onChange={v => this.changeValue(2, v)} />
</div>)
}
}
Redux
The main drawback of the above approach is that you have to pass a lot of information from the top to the bottom; as your application grows, this becomes difficult to manage. React-Redux leverages React's context feature to enable child components to access your Store directly, thus simplifying your architecture.
Example (just some key pieces of your redux application - see the react-redux documentation how to wire these together, e.g. createStore, Provider...):
// reducer.js
// Your Store is made of two reducers:
// 'dropdowns' manages the current state of your three dropdown;
// 'options' manages the list of available options.
const dropdowns = (state = [null, null, null], action = {}) => {
switch (action.type) {
case 'CHANGE_DROPDOWN_VALUE':
return state.map((v, i) => i === action.index ? action.value : v);
default:
return state;
}
};
const options = (state = [], action = {}) => {
// reducer code for option list omitted for sake of simplicity
};
// actionCreators.js
export const changeDropdownValue = (index, value) => ({
type: 'CHANGE_DROPDOWN_VALUE',
index,
value
});
// helpers.js
export const selectOptionsForDropdown = (state, index) => {
return state.options.map(({value, label}) => {
const selectedIndex = state.dropdowns.indexOf(value);
const disabled = selectedIndex >= 0 && selectedIndex !== index;
return {value, label, disabled};
});
};
// components.js
import React from 'react';
import { connect } from 'react-redux';
import { changeDropdownValue } from './actionCreators';
import { selectOptionsForDropdown } from './helpers';
import { Select } from './myOtherComponents';
const mapStateToProps = (state, ownProps) => ({
value: state.dropdowns[ownProps.index],
options: selectOptionsForDropdown(state, ownProps.index)
}};
const mapDispatchToProps = (dispatch, ownProps) => ({
onChange: value => dispatch(changeDropdownValue(ownProps.index, value));
});
const ConnectedSelect = connect(mapStateToProps, mapDispatchToProps)(Select);
export const Example = () => (
<div>
<ConnectedSelect index={0} />
<ConnectedSelect index={1} />
<ConnectedSelect index={2} />
</div>
);
As you can see, the logic in the Redux example is the same as the vanilla React code. But it is not contained in the parent component, but in reducers and helper functions (selectors). An instead of top-down passing of props, React-Redux connects each individual component to the state, resulting in a simpler, more modular, easier-to-maintain code.
The following help me to setup communication between two siblings. The setup is done in their parent during render() and componentDidMount() calls.
class App extends React.Component<IAppProps, IAppState> {
private _navigationPanel: NavigationPanel;
private _mapPanel: MapPanel;
constructor() {
super();
this.state = {};
}
// `componentDidMount()` is called by ReactJS after `render()`
componentDidMount() {
// Pass _mapPanel to _navigationPanel
// It will allow _navigationPanel to call _mapPanel directly
this._navigationPanel.setMapPanel(this._mapPanel);
}
render() {
return (
<div id="appDiv" style={divStyle}>
// `ref=` helps to get reference to a child during rendering
<NavigationPanel ref={(child) => { this._navigationPanel = child; }} />
<MapPanel ref={(child) => { this._mapPanel = child; }} />
</div>
);
}
}
I'm having trouble figuring out how to short circuit rendering a branch
of a tree of React components using Immutable.js cursors.
Take the following example:
import React from 'react';
import Immutable from 'immutable';
import Cursor from 'immutable/contrib/cursor';
let data = Immutable.fromJS({
things: [
{title: '', key: 1},
{title: '', key: 2}
]
});
class Thing extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.thing.deref() !== nextProps.thing.deref();
}
handleChangeTitle(e) {
this.props.thing.set('title', e.target.value);
}
render() {
return <div>
<input value={this.props.thing.get('title')}
onChange={this.handleChangeTitle.bind(this)} />
</div>;
}
}
class Container extends React.Component {
render() {
const cursor = Cursor.from(this.props.data, 'things', newThings => {
data.set('things', newThings);
renderContainer();
});
const things = cursor.map(thing => (
<Thing thing={thing} key={thing.get('key')} />
));
return <div>
{things}
</div>;
}
}
const renderContainer = () => {
React.render(<Container data={data} />, document.getElementById('someDiv'));
};
Say I change the first Thing's title. Only the first Thing will render with
the new title and the second Thing will not re-render due to
shouldComponentUpdate. However, if I change the second Thing's title, the
first Thing's title will go back to '' since the second Thing's cursor
is still pointing at an older version of the root data.
We update the cursors on each render of Container but the ones that don't
render due to shouldComponentUpdate also don't get the new cursor with the updated
root data. The only way I can see keeping the cursors up to date is to remove
shouldComponentUpdate in the Thing component in this example.
Is there a way to change this example to use shouldComponentUpdate using fast referential
equality checks but also keep the cursors updated?
Or, if that's not possible, could you provide an overview of how you would generally work with cursors + React components and rendering only components with updated data?
I updated your code, see comments inline:
class Thing extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.thing.deref() !== nextProps.thing.deref();
}
handleChangeTitle(e) {
// trigger method on Container to handle update
this.props.onTitleChange(this.props.thing.get('key'), e.target.value);
}
render() {
return <div>
<input value={this.props.thing.get('title')}
onChange={this.handleChangeTitle.bind(this)} />
</div>;
}
}
class Container extends React.Component {
constructor() {
super();
this.initCursor();
}
initCursor() {
// store cursor as instance variable to get access from methods
this.cursor = Cursor.from(data, 'things', newThings => {
data = data.set('things', newThings);
// trigger re-render
this.forceUpdate();
});
}
render() {
const things = this.cursor.map(thing => (
<Thing thing={thing} key={thing.get('key')} onTitleChange={this.onTitleChange.bind(this)} />
));
return <div>
{things}
</div>;
}
onTitleChange(key, title){
// update cursor to store changed things
this.cursor = this.cursor.update(x => {
// update single thing
var thing = x.get(key - 1).set('title', title);
// return updated things
return x.set(key - 1,thing);
});
}
}
const renderContainer = () => {
React.render(<Container data={data} />, document.getElementById('someDiv'));
};