Conditional render is not updating with setState() - javascript

I have a form that I would like to update depending on the state of a toggle in that form. Here is my container component with the form:
import React, { Component } from 'react';
import {
Create,
Edit,
NumberInput,
SimpleForm,
TextInput
} from 'admin-on-rest';
import LatLngInput from '../customInputs/LatLngInput';
import UTMInput from '../customInputs/UTMInput';
import UTMSwitch from '../customInputs/UTMSwitch';
export class LocationEdit extends Component {
constructor(props) {
super(props);
this.state = {
utmInput: false,
utm: {
easting: 0,
northing: 0,
isSouthern: false,
zone: 0
}
};
}
toggleUTM() {
this.setState(prevState => ({ utmInput: !prevState.utmInput }), () => {
console.log(this.state.utmInput);
});
}
render() {
return (
<Edit title={<LocationTitle />} {...this.props}>
<SimpleForm validate={validateLocationCreation}>
<TextInput source="name" />
{this.state.utmInput ? (
<UTMInput />
) : (
<LatLngInput />
)}
<UTMSwitch defaultToggled={this.state.utmInput} onToggle={this.toggleUTM.bind(this)}/>
</SimpleForm>
</Edit>
);
}
}
The toggle is supposed to switch between two input fields: UTMInput and LatLngInput. These are currently pretty similar with the exception of their labels:
UTMInput:
import React from 'react';
import { Field } from 'redux-form';
import { NumberInput } from 'admin-on-rest';
const UTMInput = () => (
<span>
<Field name="latitude" component={NumberInput} label="Northing" />
<br />
<Field name="longitude" component={NumberInput} label="Easting" />
</span>
);
export default UTMInput;
LatLngInput:
import React from 'react';
import { Field } from 'redux-form';
import { NumberInput } from 'admin-on-rest';
const LatLngInput = () => (
<span>
<Field name="latitude" component={NumberInput} label="Latitude" />
<br />
<Field name="longitude" component={NumberInput} label="Longitude" />
</span>
);
export default LatLngInput;
I understand that setState by itself does not guarantee a UI update, which is why I've included the console.log(this.state.utmInput) as a callback to the setState function in toggleUTM (this logs alternating true and false to the console as expected). I've also tried the other form of setState which accepts a function instead of an object, as the React docs suggest. I've even tried using this.forceUpdate() after the console.log(this.state.utmInput). None of these have worked.
I've used breakpoints in VSCode and I can confirm that the setState function is being called each time I toggle the switch but the conditional ({this.state.utmInput ?...) is not being called. I'm using the admin-on-rest framework, although I don't know if this is the issue. I've checked the source code and shouldComponentUpdate is not being used in any of the components I've imported from that framework (<Edit />, <SimpleForm />, <TextInput /> or <NumberInput />).
Any help would be very much appreciated!

I tried putting together an example repo that reproduces the error, but it worked fine in this new example. Turns out, despite my best guess, the Admin On Rest framework was the problem.
Upgrading to version 1.4.0 from 1.3.2 solved my issue.

Related

Update React Component When Using react-responsive-tabs

In my react app when I make a serverside update I return a response which I use to update the state of the parent component. But for my components where I use react-responsive-tabs they don't get updated.
Here's my react code:
import React, {Component, Fragment} from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import PageTitle from '../../../Layout/AppMain/PageTitle';
import {
faAngleUp,
faAngleDown,
faCommentDots,
faBullhorn,
faBusinessTime,
faCog
} from '#fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '#fortawesome/react-fontawesome';
import Tabs from 'react-responsive-tabs';
import Roles from './Roles';
import Priviledges from './Priviledges';
export default class Apage extends Component {
constructor(props) {
super(props);
this.state = {
api: this.props.api,
session: this.props.session
}
}
componentWillMount() {
this.tabsContent = [
{
title: 'Roles',
content: <Roles api={this.state.api} session={this.state.session} />
},
{
title: 'Priviledges',
content: <Priviledges api={this.state.api} session={this.state.session} />
}
];
}
getTabs() {
return this.tabsContent.map((tab, index) => ({
title: tab.title,
getContent: () => tab.content,
key: index,
}));
}
onTabChange = selectedTabKey => {
this.setState({ selectedTabKey });
};
render() {
return (
<Fragment>
<PageTitle
heading="Roles & Priviledges"
subheading=""
icon="lnr-apartment icon-gradient bg-mean-fruit"
/>
<Tabs selectedTabKey={this.state.selectedTabKey} onChange={this.onTabChange} tabsWrapperClass="body-tabs body-tabs-layout" transform={false} showInkBar={true} items={this.getTabs()}/>
</Fragment>
)
}
}
I have tried using this within my <Roles /> tag:
shouldComponentUpdate(nextProps, nextState) {
return nextProps.session!= this.props.session;
}
but I couldn't get it to work for me. Any clue?
I'm running my React JS within laravel using laravel-mix. I actually intend to update a dropdown whenever I submit a form using setState. I've done this many other times when I use React JSas a REST API.
I ended up using socket IO to trigger a setSate within my component after a response comes from the server. Although i'd prefer something neater.
You need to onChange like this - onChange={() => this.onTabChange()}
see below-
<Tabs onChange={() => this.onTabChange()} selectedTabKey={this.state.selectedTabKey} tabsWrapperClass="body-tabs body-tabs-layout" transform={false} showInkBar={true} items={this.getTabs()}/>

How to pass props to custom components in react-select

I am trying to use a custom component as an input field in react-select. Since I need validation I am trying to use HTML5 oninvalid (onInvalid in JSX) for my input tag and set the custom message for oninvalid. However I am unable to pass the message as a prop to the component that I am setting in select. Below is my code.
Input.js
import React from "react";
import { components } from "react-select";
export default class Input extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.log("component mounted");
}
setInvalidMessage = event => {
event.target.setCustomValidity("Custom Message");
};
render() {
if (this.props.isHidden) {
return <components.Input {...this.props} />;
}
return (
<components.Input
{...this.props}
required
onInvalid={this.setInvalidMessage}
/>
);
}
}
example.js
import React from "react";
import Input from "./Input";
import Select from "react-select";
import { colourOptions } from "./docs/data";
const InputBoxWithText = props => {
return <Input {...props} />;
};
export default () => (
<form>
<Select
closeMenuOnSelect={true}
components={{ InputBoxWithText }}
options={colourOptions}
/>
<input type="submit" />
</form>
);
If I pass Input in components attribute I get the hard coded message in Input.js. If I pass InputBoxWithText I don't see Input mounting at all.
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Example from './example';
ReactDOM.render(
<Example />,
document.getElementById('root')
);
Here is CodeSandBox.io URL.
Can any one let me know if what am I doing wrong.
It's better to pass custom props via select:
props.selectProps
To avoid re-creating of Custom component each time Select updates, what may cause unexpected bugs.
In my case I was passing errors in such way:
<Select
defaultValue={values}
selectProps={{ errors }}
isMulti
options={inventoryList}
onChange={changeTreeElement}
// #ts-ignore
styles={colourStyles}
/>
Then access it like selectProps.selectProps.errors in colourStyles methods.
I managed to pass my custom props using an arrow function
See docs for defining components
const Input = (inputProps: InputProps) => (
<components.Input {...inputProps} />
);
<Select
closeMenuOnSelect={true}
options={colourOptions}
components={{Input}}
/>
I don't have the solution (i'm looking for the same thing as well), but you example has multiple errors.
To load the input you have to write components={{ Input: InputBoxWithText }}, since the component name for Input is not InputBoxWithText.
Also the onInvalid does not seem to be part of the Input API, so it will never trigger. Are you trying to use the <input oninvalid="" />..?
In version 5 the only way to use custom props with typescript is to use module augmentation.
So in my project I opened react-app-env.d.ts and added there this:
import { GroupBase } from 'react-select'
declare module 'react-select/dist/declarations/src/Select' {
export interface Props<Option, IsMulti extends boolean, Group extends GroupBase<Option>> {
customOnClear: () => void;
}
}
You pass the prop to the select like this:
import Select from "react-select";
<Select customOnClear={() => {/* Your custom clear */} />
And use it in your custom component like this:
const ClearIndicator = ({ selectProps }: ClearIndicatorProps<Option, false>) => {
const { customOnClear } = selectProps
return <InputClear onClick={customOnClear} />
}
Docs:
https://react-select.com/typescript#custom-select-props

React-Loadable re-rendering causing input to lose focus

I'm having an issue where react-loadable is causing one of my input components to re-render and lose focus after a state update. I've done some digging and I can't find anyone else having this issue, so I think that I'm missing something here.
I am attempting to use react-loadable to dynamically include components into my app based on a theme that the user has selected. This is working fine.
./components/App
import React from 'react';
import Loadable from 'react-loadable';
/**
* Import Containers
*/
import AdminBar from '../../containers/AdminBar';
import AdminPanel from '../../components/AdminPanel';
import 'bootstrap/dist/css/bootstrap.css';
import './styles.css';
const App = ({ isAdmin, inEditMode, theme }) => {
const MainContent = Loadable({
loader: () => import('../../themes/' + theme.name + '/components/MainContent'),
loading: () => (<div>Loading...</div>)
});
const Header = Loadable({
loader: () => import('../../themes/' + theme.name + '/components/Header'),
loading: () => (<div>Loading...</div>)
});
return (
<div>
{
(isAdmin) ? <AdminBar
className='admin-bar'
inEditMode={inEditMode} /> : ''
}
<Header
themeSettings={theme.settings.Header} />
<div className='container-fluid'>
<div className='row'>
{
(isAdmin && inEditMode) ? <AdminPanel
className='admin-panel'
theme={theme} /> : ''
}
<MainContent
inEditMode={inEditMode} />
</div>
</div>
</div>
);
};
export default App;
./components/AdminPanel
import React from 'react';
import Loadable from 'react-loadable';
import './styles.css';
const AdminPanel = ({ theme }) => {
const ThemedSideBar = Loadable({
loader: () => import('../../themes/' + theme.name + '/components/SideBar'),
loading: () => null
});
return (
<div className='col-sm-3 col-md-2 sidebar'>
<ThemedSideBar
settings={theme.settings} />
</div>
);
};
export default AdminPanel;
This is what my <ThemedSideBar /> components looks like:
./themes/Default/components/SideBar
import React from 'react';
import ThemeSettingPanel from '../../../../components/ThemeSettingPanel';
import ThemeSetting from '../../../../containers/ThemeSetting';
import './styles.css';
const SideBar = ({ settings }) => {
return (
<ThemeSettingPanel
name='Header'>
<ThemeSetting
name='Background Color'
setting={settings.Header}
type='text'
parent='Header' />
<ThemeSetting
name='Height'
setting={settings.Header}
type='text'
parent='Header' />
</ThemeSettingPanel>
);
};
export default SideBar;
./components/ThemeSettingPanel
import React from 'react';
import { PanelGroup, Panel } from 'react-bootstrap';
const ThemeSettingPanel = ({ name, children }) => {
return (
<PanelGroup accordion id='sidebar-accordion-panelGroup'>
<Panel>
<Panel.Heading>
<Panel.Title toggle>{name}</Panel.Title>
</Panel.Heading>
<Panel.Body collapsible>
{children}
</Panel.Body>
</Panel>
</PanelGroup>
);
};
export default ThemeSettingPanel;
./containers/ThemeSetting
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { themeSettingChange } from '../App/actions';
import ThemeSetting from '../../components/ThemeSetting';
class ThemeSettingContainer extends Component {
constructor(props) {
super(props);
this.handleOnChange = this.handleOnChange.bind(this);
}
handleOnChange(name, parent, value) {
const payload = {
name: name,
parent,
value: value
};
this.props.themeSettingChange(payload);
}
render() {
return (
<ThemeSetting
name={this.props.name}
setting={this.props.setting}
parent={this.props.parent}
type={this.props.type}
handleOnChange={this.handleOnChange} />
);
}
}
//----Redux Mappings----//
const mapStateToProps = (state) => ({
});
const mapDispatchToProps = {
themeSettingChange: (value) => themeSettingChange(value)
};
export default connect(mapStateToProps, mapDispatchToProps)(ThemeSettingContainer);
./component/ThemeSetting
import React from 'react';
import TextField from '../common/TextField';
import './styles.css';
const ThemeSetting = ({ name, setting, type, parent, handleOnChange }) => {
return (
<div className='row theme-setting'>
<div className='col-xs-7'>
{name}
</div>
<div className='col-xs-5'>
{
generateField(type, setting, name, parent, handleOnChange)
}
</div>
</div>
);
};
function generateField(type, setting, name, parent, handleOnChange) {
const value = setting ? setting[name] : '';
switch (type) {
case 'text':
return <TextField
value={value}
name={name}
parent={parent}
handleOnChange={handleOnChange} />;
default:
break;
}
}
export default ThemeSetting;
./components/common/TextField
import React from 'react';
import { FormControl } from 'react-bootstrap';
const TextField = ({ value, name, parent, handleOnChange }) => {
return (
<FormControl
type='text'
value={value}
onChange={(e) => {
handleOnChange(name, parent, e.target.value);
}} />
);
};
export default TextField;
When a field inside of my Admin Panel is updated, a state change is triggered. It seems like this triggers react-loadable to re-render my <ThemedSideBar /> components which destroys my input and creates a new one with the updated value. Has anyone else had this issue? Is there a way to stop react-loadable from re-rendering?
EDIT: Here is the requested link to the repo.
EDIT: As per conversation in the comments, my apologies, I misread the question. Answer here is updated (original answer below updated answer)
Updated answer
From looking at the react-loadable docs, it appears that the Loadable HOC is intended to be called outside of a render method. In your case, you are loading ThemedSideBar in the render method of AdminPanel. I suspect that the change in your TextEdit's input, passed to update your Redux state, and then passed back through the chain of components was causing React to consider re-rendering AdminPanel. Because your call to Loadable was inside the render method (i.e. AdminPanel is a presentational component), react-loadable was presenting a brand new loaded component every time React hit that code path. Thus, React thinks it needs to destroy the prior component to appropriately bring the components up to date with the new props.
This works:
import React from 'react';
import Loadable from 'react-loadable';
import './styles.css';
const ThemedSideBar = Loadable({
loader: () => import('../../themes/Default/components/SideBar'),
loading: () => null
});
const AdminPanel = ({ theme }) => {
return (
<div className='col-sm-3 col-md-2 sidebar'>
<ThemedSideBar
settings={theme.settings} />
</div>
);
};
export default AdminPanel;
Original answer
It seems that your problem is likely related to the way you've built TextField and not react-loadable.
The FormControl is taking value={value} and the onChange handler as props. This means you've indicated it is a controlled (as opposed to uncontrolled) component.
If you want the field to take on an updated value when the user types input, you need to propagate the change caught by your onChange handler and make sure it gets fed back to the value in the value={value} prop.
Right now, it looks like value will always be equal to theme.settings.Height or the like (which is presumably null/empty).
An alternative would be to make that FormControl an uncontrolled component, but I'm guessing you don't want to do that.

react-ultimate-pagination component setup

I'm trying to use this package react-ultimate-pagination: https://github.com/ultimate-pagination/react-ultimate-pagination
I want to set it up like their basic demo example: https://codepen.io/dmytroyarmak/pen/GZwKZJ
The usage instructions at the bottom of the github page say to import the component like this:
import ReactUltimatePagination from 'react-ultimate-pagination';
But the codepen demo just shows a constant:
const UltimatePagination = reactUltimatePaginationBasic.default;
I copied the code from the demo, but since it is mismatched with the import, I have an error of UltimatePagination being undefined and reactUltimatePaginationBasic undefined.
Does anyone know how to set up this component like the demo example?
The module exports the higher oder component createUltimatePagination as a named export. To import it using es6 import syntax it has to be the following:
import {createUltimatePagination} from 'react-ultimate-pagination';
Example App:
import React, { Component } from "react";
import { render } from "react-dom";
import { createUltimatePagination } from "react-ultimate-pagination";
const Button = ({ value, isActive, disabled, onClick }) => (
<button
style={isActive ? { fontWeight: "bold" } : null}
onClick={onClick}
disabled={disabled}
>
{value}
</button>
);
const PaginatedPage = createUltimatePagination({
itemTypeToComponent: {
PAGE: Button,
ELLIPSIS: () => <Button value="..." />,
FIRST_PAGE_LINK: () => <Button value="First" />,
PREVIOUS_PAGE_LINK: () => <Button value="Prev" />,
NEXT_PAGE_LINK: () => <Button value="Next" />,
LAST_PAGE_LINK: () => <Button value="Last" />
}
});
class App extends Component {
state = {
page: 1
};
render() {
return (
<PaginatedPage
totalPages={10}
currentPage={this.state.page}
onChange={page => this.setState({ page })}
/>
);
}
}
render(<App />, document.getElementById("root"));
Also see this working example on codesandbox.
To be honest I played around with the api of that library and actually it is unclear to me how this library is intended to be used. A pagination component should receive a list of items and then provide a render prop to render the current page with a slice of these items. It's a pagination that does not paginate. Basically it's only a button bar.
Just use var ReactUltimatePagination = require('react-ultimate-pagination'); after you've installed it with npm install react-ultimate-pagination --save

How to input data in one React component and have it render in another?

I'm learning React using JSX and ES6 and I've got a pretty decent handle on how to create components and route to different views using ReactRouter4.
What I still haven't been able to figure out is for example how i can create an Admin page where I input the details of a work for my portfolio and have all the works render on the another page, presumably Portfolio page.
Here's what I've got.
App.js loads the Portfolio.js component
import React from 'react';
import Navigation from './Navigation';
import Title from './Title';
import Portfolio from './Portfolio';
class App extends React.Component {
render() {
return(
<div className="container-fluid">
<div className="container">
<div className="row">
<div className="col-sm-12">
<Navigation />
<Title title="kuality.io"/>
<section className="app">
<Portfolio works={this.props.works} />
</section>
</div>
</div>
</div>
</div>
)
}
}
export default App;
The Portfolio.js component has a constructor to bind a unique method named addWork(), the React methods componentWillMount() and componentWillUnmount() to handle state, and the default render(). One more thing to mention about this component is that it's calling a component called ../base which has all the details to an online DB via Firebase. So if that's relevant as to where it is place, then take that into consideration otherwise don't sweat it.
import React from 'react';
import Work from './Work';
import Admin from './Admin';
import base from '../base';
class Portfolio extends React.Component {
constructor() {
super();
this.addWork = this.addWork.bind(this);
// getInitialState
this.state = {
works: {}
};
}
componentWillMount() {
this.ref = base.syncState(`/works`
, {
context: this,
state: 'works'
})
}
componentWillUnmount() {
base.removeBinding(this.ref);
}
addWork(work) {
// update our state
const works = {...this.state.works};
// add in our new works with a timestamp in seconds since Jan 1st 1970
const timestamp = Date.now();
works[`work-${timestamp}`] = work;
// set state
this.setState({ works });
}
render() {
return(
<div>
<section className="portfolio">
<h3>Portfolio</h3>
<ul className="list-of-work">
{
Object
.keys(this.state.works)
.map(key => <Work key={key} details={this.state.works[key]}/>)
}
</ul>
</section>
</div>
)
}
}
export default Portfolio;
Inside of the Object i'm mapping through the Work component that is just a list item I have made another component for and isn't really relevant in the question.
Finally I have the Admin.js and AddWorkForm.js components. I abstracted the AddWorkForm.js so that I could use it elsewhere if need be, basically the main idea behind React Components, so that's why I chose to do it that way.
import React from 'react';
import Title from './Title';
import AddWorkForm from './AddWorkForm';
class Admin extends React.Component {
constructor() {
super();
this.addWork = this.addWork.bind(this);
// getInitialState
this.state = {
works: {}
};
}
addWork(work) {
// update our state
const works = {...this.state.works};
// add in our new works with a timestamp in seconds since Jan 1st 1970
const timestamp = Date.now();
works[`work-${timestamp}`] = work;
// set state
this.setState({ works });
}
render() {
return(
<div className="container-fluid">
<div className="container">
<div className="row">
<div className="col-sm-12">
<Title title="Admin"/>
<section className="admin">
<AddWorkForm addWork={this.addWork} />
</section>
</div>
</div>
</div>
</div>
)
}
}
export default Admin;
and the AddWorkForm.js component which is basically a form that onSubmit creates and object and resets the form
import React from 'react';
class AddWorkForm extends React.Component {
createWork(event) {
event.preventDefault();
console.log('Creating some work');
const work = {
name: this.name.value,
desc: this.desc.value,
image: this.image.value
}
this.props.addWork(work);
this.workForm.reset();
}
render() {
return(
<form ref={(input) => this.workForm = input} className="work-edit form-group" onSubmit={(e) => this.createWork(e)}>
<input ref={(input) => this.name = input} type="text" className="form-control" placeholder="Work Title"/>
<textarea ref={(input) => this.desc = input} type="text" className="form-control" placeholder="Work Description"></textarea>
<input ref={(input) => this.image = input} type="text" className="form-control" placeholder="Work Image"/>
<button type="submit" className="btn btn-primary">+Add Work</button>
</form>
)
}
}
export default AddWorkForm;
Here is the file that includes where I'm using ReactRouter:
import React from 'react';
import { render } from 'react-dom';
// To render one method from a package user curly brackets, you would have to know what method you wan though
import { BrowserRouter, Match, Miss} from 'react-router';
import './css/normalize.css';
import './css/bootstrap.css';
import './css/style.css';
// import '../js/bootstrap.js';
import App from './components/App';
import WorkItem from './components/WorkItem';
import Capability from './components/Capability';
import Connect from './components/Connect';
import NotFound from './components/NotFound';
import Admin from './components/Admin';
const Root = ()=> {
return(
<BrowserRouter>
<div>
<Match exactly pattern="/" component={App} />
<Match pattern="/work/:workId" component={WorkItem} />
<Match exactly pattern="/capability" component={Capability} />
<Match exactly pattern="/connect" component={Connect} />
<Match exactly pattern="/admin" component={Admin} />
<Miss component={NotFound} />
</div>
</BrowserRouter>
)
}
render (<Root />, document.querySelector('#main'));
So here's what I've tried and failed to accomplish, and it's likely some kind of this.props solution that I haven't been able to define, I need to create the work in Admin.js component, which creates the object and then have it throw that object to Portfolio.js component so it can render it via the Work.js component and it doesn't add the object to the DB.
This works when i put all the components on the same page, which isn't ideal because then anyone accessing my Portfolio could add a work. Sure I could start the process of learning authentication and how to make that component appear or disappear based on user credentials, but I'd much rather also learn the very valuable skill of being able to have my admin page on a separate view all together because I see another application for learning to do so.
Would love to hear others opinions on this and where they may be able to determine I'm failing here.
Btw, I realize I have other components like Nav.js and Title.js but they are not necessary in order to illustrate the example.
Thank you.
You can pass components as props and when using React Router you can have named components.
For data sharing between siblings is better advised to have the data on a parent component although you could use context, but this is not advised and may be unacessible on future versions.
If you need to create something on another component (don't know why) you could pass a function that would render it.

Categories