I'm having problem setting state. I've to pass extra classes to CardComponent on click, on click is working fine, setState is called, and the callback executes, but state is not mutated (as logs clearly deny that state is updated - see onSelect below). How could I fix that?
Flow is that if an item is selected, parent component resets the array with selected property set on each item (the selected result object gets selected set to true), that works fine and list is reset, selected is highlighted. Then if user clicks on another item (SearchResult component), it should update itself and apply extra classes. This time it's guarenteed that parent would not reset the list.
import { Component, ReactNode } from "react";
import { Subject, Subscription } from "rxjs";
import CardComponent from "../../utils/card.component";
export interface SearchResultComponentClickEvent {
(_id: string): void;
}
export interface SearchResultComponentProps {
result: any;
full: boolean;
onClick: SearchResultComponentClickEvent;
onSelect: Subject<string>;
}
interface SearchResultComponentState {
selected: string;
extraClasses: string;
}
export default class SearchResult extends Component<
SearchResultComponentProps,
SearchResultComponentState
> {
onSelectSubscription: Subscription = null;
constructor(props: SearchResultComponentProps) {
super(props);
let extraClasses = "mb-4";
if (this.props.result.selected) extraClasses += " border border-primary";
this.state = {
selected: this.props.result.selected,
extraClasses,
};
this.onSelect = this.onSelect.bind(this);
}
componentDidMount(): void {
this.onSelectSubscription = this.props.onSelect.subscribe((_id: string) => {
this.setState({
selected: '',
extraClasses: 'mb-4'
});
});
}
componentWillUnmount(): void {
this.onSelectSubscription.unsubscribe();
}
onSelect() {
this.setState({
selected: this.props.result._id,
extraClasses: "mb-4 border border-primary",
}, () => {
console.log(this.state);
// logs: { selected: '', extraClasses: "mb4 " }
});
this.props.onClick(this.props.result._id);
}
view(): ReactNode {
return <div>Simple view</div>
}
fullView(): ReactNode {
return <div>Extended view</div>;
}
render(): ReactNode {
return (
<CardComponent
padding={0}
onClick={this.onSelect}
extraClasses={this.state.extraClasses}
key={this.state.extraClasses}
>
{this.props.full ? this.fullView() : this.view()}
</CardComponent>
);
}
}
You have changed the state but setState is asynchronous in nature, changing the state will not immediately give its updated value https://facebook.github.io/react/docs/react-component.html#setstate, if you can show snippet of what exactly are you doing CardComponent, can give a more reliable solution.
If you still want to go with this approach, you can change your onSelect to something like this.
onSelect() {
this.setState({
selected: this.props.result._id,
extraClasses: "mb-4 border border-primary",
}, function() {
console.log(this.state);
this.props.onClick(this.props.result._id);
});
}
this will wrap or attached a callback to the state, which guarantees it, but it has its own side effects.
I would also recommend componentDidUpdate over this solution, more precise.
The problem is probably in the rxjs Subscription object.
Insert console.log and check...
componentDidMount(): void {
this.onSelectSubscription = this.props.onSelect.subscribe((_id: string) => {
// Log
console.log('componentDidMount-onSelectSubscription', this.state);
this.setState({
selected: '',
extraClasses: 'mb-4'
});
});
}
Related
import React, { Component } from "react";
export interface MyComponentProps {
show: boolean;
}
export interface MyComponentState {
show: boolean;
}
export default class App extends Component<MyComponentProps, MyComponentState> {
static defaultProps = {
show: true
};
static getDerivedStateFromProps(props: MyComponentProps) {
console.log("getDerivedStateFromProps: ", props);
if ("show" in props) {
return { show: props.show };
}
return null;
}
constructor(props: MyComponentProps) {
super(props);
this.state = {
show: props.show
};
}
onClick() {
this.setState({ show: false });
}
render() {
const { show } = this.state;
return (
<div>
{show ? "teresa teng" : ""}
<button type="button" onClick={() => this.onClick()}>
toggle
</button>
</div>
);
}
}
getDerivedStateFromProps() static method will be executed after setState(). So I click the button to try to change the value of state.show to false, but the getDerivedStateFromProps() method will change state.show to true. So the text will always be visible.
getDerivedStateFromProps intends to use the props passed in by the parent component to update the state.
How can I solve this? Playground codesandbox.
getDerviedStateFromProps is bound to run after every prop and state change. This was not an actual design but this change in functionality was introduced in React version 16.4 (if I remember correctly).
Now, if you want to update the local show i.e. your state on the basis of your props, you can:
Pass a callback which updates show for you in the parent component and then use the new prop value.(As mentioned by #jonrsharpe in the comments).
You can also make use of a key prop which tells your component to completely unmount and mount itself in case of a key change. This will lead to the state getting reset based on the value of the props.
For ex,
<App show={this.state.show}
key={this.state.show}/>
Example CodeSandBox
I'm trying to create some React components using TypeScript, one of which handles user input (let's call it MyCtrl), and the other reacting to this user input and updating the backend (MyUpdatingCtrl), both being controlled components. My issue is that when updating the backend fails I need to revert the value in the user input component, but since it keeps the value in its state I can't update it from the outer component. So how do I handle this case correctly?
Note that I simplified my situation to explain my issue more easily. The following bit of code does not represent my actual project, but illustrates the issue in the simplest way I could think of.
MyCtrl:
export interface MyCtrlProps {
Value: string;
OnValueChanged: (newValue: string) => void;
}
interface MyCtrlState {
CurrentValue: string;
}
export class MyCtrl extends React.Component<MyCtrlProps, MyCtrlState>{
constructor(props: MyCtrlProps) {
super(props);
this.state = { CurrentValue: props.Value };
}
render() {
return <input onChange={this.onInputChanged} value={this.state.CurrentValue} />;
}
private onInputChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ ...this.state, CurrentValue: e.target.value },
() => this.props.OnValueChanged(this.state.CurrentValue));
};
}
MyUpdatingCtrl:
export interface MyUpdatingCtrlProps {
Value: string;
}
interface MyUpdatingCtrlState {
CurrentValue: string;
PreviousValue: string;
}
export class MyUpdatingCtrl extends React.Component<MyUpdatingCtrlProps, MyUpdatingCtrlState>{
constructor(props: MyUpdatingCtrlProps) {
super(props);
this.state = {
CurrentValue: props.Value,
PreviousValue: props.Value
};
}
render() {
return <MyCtrl Value={this.state.CurrentValue} OnValueChanged={this.onValueChanged} />
}
private onValueChanged = (newValue: string): void => {
try {
// update backend
}
catch (error) {
// How do I reset the value of MyCtrl to the previous value here?
}
};
}
Setting the state.CurrentValue in the catch block inside MyUpdatingCtrl.onValueChanged of course won't update the value in MyCtrl, so what should I do?
first of all, you can't get the updated state like that, use
this.props.OnValueChanged(e.target.value);
because setState is an async function and can't get its updated value in the function you are calling it except by using the callback method
I'm trying to implement a handler, in React, for a survey implemented in SurveyJS. This is for multiple-choice questions that may have answers like "None of the Above" or "Prefer Not To Answer". If one of those answers is selected, all other answers should be blanked out, and if a different answer is selected, these checkboxes should be cleared out. I'm doing fine with either one of these individually, but having problems with a question where both options are present, specifically when switching back & forth between the two special options.
What I think is happening is that when one answer triggers the handler, and unchecks the other checkbox, it triggers the handler again. My solution is to set a state that indicates when the handler is in the middle of this process, and not do it again at that time.
I got a JS solution for this here: https://github.com/surveyjs/editor/issues/125 - and below is my attempt to convert it to React. (Just the relevant parts of the code included.)
However, on compile, it gives the following error:
ERROR in [at-loader] ./src/components/Survey/SurveyContainer.tsx:55:19
TS2339: Property 'ValueChanging' does not exist on type
'Readonly<{}>'.
I can't find anything about this specific error. Other references to the state (i.e. where I'm setting it) are working. Why can't I read it?
Thanks!
Component:
import * as React from 'react';
import { Component } from 'react';
import { Survey, surveyStrings } from 'survey-react';
import 'whatwg-fetch';
import Marked from '../Marked';
import * as style from './style';
interface Props {
surveyJson: object;
complete: boolean;
resultMarkdown: string;
surveyTitle: string;
sendSurveyAnswers: (answers: object[]) => void;
noneOfTheAboveHandler: (survey: object, options: object) => void;
}
Survey.cssType = 'standard';
surveyStrings.progressText = '{0}/{1}';
surveyStrings.emptySurvey = '';
export default class SurveyComponent extends Component<Props, {}> {
render() {
const { surveyJson, sendSurveyAnswers, complete, surveyTitle, resultMarkdown, noneOfTheAboveHandler } = this.props;
return (
<style.Wrapper>
{ surveyJson && (!complete) &&
<style.SurveyWrapper>
<style.SurveyTitle>{ surveyTitle }</style.SurveyTitle>
<style.Survey>
<Survey
onValueChanged={ noneOfTheAboveHandler }
css={ style.surveyStyles }
json={ surveyJson }
onComplete={ sendSurveyAnswers }
/>
</style.Survey>
</style.SurveyWrapper>
}
{ complete &&
<style.Results>
<Marked content={resultMarkdown} />
</style.Results>
}
</style.Wrapper>
);
}
}
Container:
import * as React from 'react';
import { Component } from 'react';
import { connect } from 'react-redux';
import SurveyComponent from './SurveyComponent';
interface Props {
id: string;
surveyJson: object;
complete: boolean;
resultMarkdown: string;
surveyTitle: string;
getSurveyQuestions: (id: string) => void;
sendSurveyAnswers: (answers: object[]) => void;
noneOfTheAboveHandler: (survey: object, options: object) => void;
clearSurvey: () => void;
}
class SurveyContainer extends Component<Props, {}> {
constructor(props) {
super(props);
this.state = { ValueChanging: false };
}
componentDidMount() {
this.noneOfTheAboveHandler = this.noneOfTheAboveHandler.bind(this);
this.props.getSurveyQuestions(this.props.id);
}
specialValueSelected(options, specialValue) {
const { question } = options;
const prevValue = question.prevValue;
const index = options.value.indexOf(specialValue);
this.setState({ ValueChanging: true });
//has special value selected
if(index > -1) {
//special value was selected before
if(prevValue.indexOf(specialValue) > -1) {
var value = question.value;
value.splice(index, 1);
question.value = value;
} else {
//special value select just now
question.value = [specialValue];
}
}
this.setState({ ValueChanging: false });
return index > -1;
}
noneOfTheAboveHandler(survey, options) {
const none = 'NA';
const preferNotToAnswer = 'PN';
const { question } = options;
if(this.state.ValueChanging) {
return;
}
if (!question || question.getType() !== 'checkbox') {
return;
}
if (!question.prevValue || !options.value) {
question.prevValue = options.value;
return;
}
if (!this.specialValueSelected(options,none)) {
this.specialValueSelected(options,preferNotToAnswer);
}
question.prevValue = options.value;
}
componentWillUnmount() {
this.props.clearSurvey();
}
render() {
return (
<SurveyComponent
noneOfTheAboveHandler={this.noneOfTheAboveHandler}
{...this.props}
/>
);
}
}
const mapStateToProps = (state, ownProps) => ({
surveyJson: state.survey.json,
answers: state.survey.answers,
resultMarkdown: state.survey.resultMarkdown,
complete: state.survey.complete,
surveyTitle: state.page && state.page.data ? state.page.data.title : ''
});
const mapDispatchToProps = dispatch => ({
getSurveyQuestions: id => dispatch({ type: 'GET_SURVEY_QUESTIONS', id }),
sendSurveyAnswers: answers => dispatch({ type: 'SEND_SURVEY_ANSWERS', answers: answers.data }),
clearSurvey: () => dispatch({ type: 'CLEAR_SURVEY' })
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(SurveyContainer);
The most likely cause for this is that you don't specify the type for your component's state in its class definition, so it defaults to {}. You can fix it by declaring interfaces for the types of props and state and providing these as type arguments to React.Component:
interface MyComponentProps { /* declare your component's props here */ }
interface MyComponentState { ValueChanging : boolean }
class MyComponent extends React.Component<MyComponentProps, MyComponentState> {
constructor(props) {
...
You could provide the types directly between the < and >, but using interfaces usually leads to more readable code and promotes reuse. To prevent confusion with components it's also a good idea to use lower-case identifiers for the properties on props and state.
FYI: We have introduce this functionality into SurveyJS Library sometimes ago.
Here is the react example with "Select All" and "None of the Above" features, out of the box, without custom code.
Thank you!
When submitting a form, I wish to show a small popup for 2.5 seconds if the server sends back a bad response.
The logic is fairly simple, however, I cannot figure out how to make this popup listen to a boolean somewhere in the state management(MobX in my case). I can get the content into the Popup just fine, however, the trigger is a button(and the content will show, if you click it) - But how do I make it listen to a boolean value somewhere?
Fairly simple class here:
import React from "react";
import { Popup, Button } from "semantic-ui-react";
import { inject } from "mobx-react";
const timeoutLength = 2500;
#inject("store")
export default class ErrorPopup extends React.Component {
state = {
isOpen: false
};
handleOpen = () => {
this.setState({
isOpen: true
});
this.timeout = setTimeout(() => {
this.setState({
isOpen: false
})
}, timeoutLength)
};
handleClose = () => {
this.setState({
isOpen: false
});
clearTimeout(this.timeout)
};
render () {
const errorContent = this.props.data;
if(errorContent){
return(
<Popup
trigger={<Button content='Open controlled popup' />}
content={errorContent}
on='click'
open={this.state.isOpen}
onClose={this.handleClose}
onOpen={this.handleOpen}
position='top center'
/>
)
}
}
}
However, the trigger value is a button which is rendered if this.props.data is present. But that's not the behavior I wish; I simply want the popup to render(and thus trigger) if this.props.data is there; alternatively, I can provide a true value with props if need be.
But how do I make this component trigger without it being a hover/button?
How about passing in the isOpen prop? Then you could add some logic onto the componentWillReceiveProps hook:
import React from "react";
import { Popup, Button } from "semantic-ui-react";
import { inject } from "mobx-react";
const timeoutLength = 2500;
#inject("store")
export default class ErrorPopup extends React.Component {
constructor(props) {
super(props);
this.state = {
isOpen: false,
}
};
//This is where you trigger your methods
componentWillReceiveProps(nextProps){
if(true === nextProps.isOpen){
this.handleOpen();
} else {
this.handleClose();
}
}
handleOpen = () => {
this.setState({
isOpen: true
});
this.timeout = setTimeout(() => {
//No need to repeat yourself - use the existing method here
this.handleClose();
}, timeoutLength)
};
handleClose = () => {
this.setState({
isOpen: false
});
clearTimeout(this.timeout)
};
render () {
const errorContent = this.props.data;
if(errorContent){
return(
<Popup
trigger={<Button content='Open controlled popup' />}
content={errorContent}
on='click'
open={this.state.isOpen}
position='top center'
/>
)
}
}
}
Without a need of handling delay - you could simply pass in the isOpen prop and that would do the trick.
And here what it could look like in your parent component's render:
let isOpen = this.state.isOpen;
<ErrorPopup isOpen={isOpen}/>
Set this value to control the popup, ideally, this should be a part of your parent component's state. Having a stateful component as a parent is important to make the popup re-rendered
Just use the property open as in the documentation. There is a separate example when popup is opened by default.
In my component, there are local state of paid and droppedItems.
constructor () {
super();
this.state = {
droppedItems: [],
};
And I specified drop specification like below,
drop(props, monitor) {
const orderItem = monitor.getItem();
},
What I want to do is that when an orderItem is dropped to the target, it pushes orderItem to state of droppedItems So to do that, I made handleDrop function
handleDrop (orderItem) {
if(!orderItem){
this.setState(update(this.state, {
droppedItems: orderItem ? {
$push: [orderItem]
} : {}
}));
}
else return false;
}
And call it like this
return connectDropTarget(
<div className="payment_block" onDrop={this.handleDrop}>
In my render function, there are const { canDrop, connectDropTarget, getItem} = this.props;
Below is result from console.log(this.props)
Object { id: 1, order: Object, channel: undefined, dispatch: routerMiddleware/</</<(), connectDropTarget: wrapHookToRecognizeElement/<(), canDrop: false, itemType: "order_item", getItem: Object
Although it recognizes that there is a dropped item, it does not push to the state.
How can I push above Object of getItem to the component's state? What am I doing wrong here?
Thanks in advance
The signature of drop also receives your dnd decorated component as its third argument, so you should be able to call your handleAdd method.
drop(props, monitor, component) {
component.handleDrop(monitor.getItem());
}
Also the if statement in your handleDrop method is checking that no orderItem is passed in, which means the code will never run when you want it to, I'm not familiar with the what your update function does (presumably immutably pushing to the array), but perhaps something like this would work...
handleDrop: function(orderItem) {
if (!!orderItem) {
this.setState(
update(this.state, { droppedItems: { $push: [orderItem] } })
)
}
}