React props wont change manually, how to get around restrictions? - reactjs

When the component renders it has some children components and when i log props in the formgroup it looks like what's below.
{$$typeof: Symbol(react.element), key: null, ref: null, props: {…}, type: ƒ, …}
import React from 'react';
class FormGroup extends React.Component {
constructor(props) {
super(props)
this.state = {
value: {}
}
}
setValue() {
//do nothing at the moment
}
render() {
return (<div className="formGroup" >
{
this.props.children.map(child => {
child.props.setValue = this.setValue;
console.log('child', child)
// does not change
return child;
})
}
</div>)
}
};
export default FormGroup;

You can utilize React Children and cloneElement API to manipulate children's props.
import React from 'react';
class FormGroup extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {},
};
}
setValue() {
//do nothing at the moment
}
render() {
const { children } = this.props;
return (
<div className="formGroup">
{React.Children.map(children, (child) =>
React.cloneElement(child, {
...child.props,
setValue: this.setValue,
})
)}
</div>
);
}
}
export default FormGroup;

Related

Child component not triggering rendering when parent injects props with differnt values

Here are my components:
App component:
import logo from './logo.svg';
import {Component} from 'react';
import './App.css';
import {MonsterCardList} from './components/monster-list/monster-card-list.component'
import {Search} from './components/search/search.component'
class App extends Component
{
constructor()
{
super();
this.state = {searchText:""}
}
render()
{
console.log("repainting App component");
return (
<div className="App">
<main>
<h1 className="app-title">Monster List</h1>
<Search callback={this._searchChanged}></Search>
<MonsterCardList filter={this.state.searchText}></MonsterCardList>
</main>
</div>
);
}
_searchChanged(newText)
{
console.log("Setting state. new text: "+newText);
this.setState({searchText:newText}, () => console.log(this.state));
}
}
export default App;
Card List component:
export class MonsterCardList extends Component
{
constructor(props)
{
super(props);
this.state = {data:[]};
}
componentDidMount()
{
console.log("Component mounted");
this._loadData();
}
_loadData(monsterCardCount)
{
fetch("https://jsonplaceholder.typicode.com/users", {
method: 'GET',
}).then( response =>{
if(response.ok)
{
console.log(response.status);
response.json().then(data => {
let convertedData = data.map( ( el, index) => {
return {url:`https://robohash.org/${index}.png?size=100x100`, name:el.name, email:el.email}
});
console.log(convertedData);
this.setState({data:convertedData});
});
}
else
console.log("Error: "+response.status+" -> "+response.statusText);
/*let data = response.json().value;
*/
}).catch(e => {
console.log("Error: "+e);
});
}
render()
{
console.log("filter:" + this.props.filter);
return (
<div className="monster-card-list">
{this.state.data.map((element,index) => {
if(!this.props.filter || element.email.includes(this.props.filter))
return <MonsterCard cardData={element} key={index}></MonsterCard>;
})}
</div>
);
}
}
Card component:
import {Component} from "react"
import './monster-card.component.css'
export class MonsterCard extends Component
{
constructor(props)
{
super(props);
}
render()
{
return (
<div className="monster-card">
<img className="monster-card-img" src={this.props.cardData.url}></img>
<h3 className="monster-card-name">{this.props.cardData.name}</h3>
<h3 className="monster-card-email">{this.props.cardData.email}</h3>
</div>
);
}
}
Search component:
import {Component} from "react"
export class Search extends Component
{
_searchChangedCallback = null;
constructor(props)
{
super();
this._searchChangedCallback = props.callback;
}
render()
{
return (
<input type="search" onChange={e=>this._searchChangedCallback(e.target.value)} placeholder="Search monsters"></input>
);
}
}
The problem is that I see how the text typed in the input flows to the App component correctly and the callback is called but, when the state is changed in the _searchChanged, the MonsterCardList seems not to re-render.
I saw you are using state filter in MonsterCardList component: filter:this.props.searchText.But you only pass a prop filter (filter={this.state.searchText}) in this component. So props searchTextis undefined.
I saw you don't need to use state filter. Replace this.state.filter by this.props.filter
_loadData will get called only once when the component is mounted for the first time in below code,
componentDidMount()
{
console.log("Component mounted");
this._loadData();
}
when you set state inside the constructor means it also sets this.state.filter for once. And state does not change when searchText props change and due to that no rerendering.
constructor(props)
{
super(props);
this.state = {data:[], filter:this.props.searchText};
}
If you need to rerender when props changes, use componentDidUpdate lifecycle hook
componentDidUpdate(prevProps)
{
if (this.props.searchText !== prevProps.searchText)
{
this._loadData();
}
}
Well, in the end I found what was happening. It wasn't a react related problem but a javascript one and it was related to this not been bound to App class inside the _searchChanged function.
I we bind it like this in the constructor:
this._searchChanged = this._searchChanged.bind(this);
or we just use and arrow function:
_searchChanged = (newText) =>
{
console.log("Setting state. new text: "+newText);
this.setState({filter:newText}, () => console.log(this.state));
}
Everything works as expected.

TypeError: Cannot read property 'vote' of null in render view

In this component I get a Promise object in the properties, I try to put it in state, but when the view is rendered, I get the message "TypeError: Cannot read property 'vote' of null", asking for a solution to my problem, I spent two hours on it and I don't see the end. What should I do differently?
import { IVoteDetailsProps } from "./IVoteDetailsProps";
import { IVoteDetailsState } from "./IVoteDetailsState";
export class VoteDetails extends React.Component<IVoteDetailsProps, IVoteDetailsState>{
constructor(props: IVoteDetailsProps) {
super();
console.log(props)
}
componentDidMount() {
let data = this.props.voteDetails;
data.then(result => this.setState({
vote: result
}));
};
public render(): React.ReactElement<IVoteDetailsState> {
return (
<table >
<tbody>
{this.state.vote && this.state.vote.map(el => {
<tr id={el.id.toString()}>
<td>{el.title}</td>
<td>{el.voteType}</td>
</tr>
})}
</tbody>
</table>
)
}
}
export interface IVoteDetailsProps {
voteDetails: Promise<IVoteDetailsData[]>;
}
export interface IVoteDetailsData{
id: number;
title: string;
voteType: string;
}
import React = require("react");
import { VoteDetails } from "../VoteDetails/VoteDetails";
import { IVoteListProps } from "./IVoteListProps";
export class VoteList extends React.Component<IVoteListProps, {}> {
constructor(props: IVoteListProps) {
super(props);
console.log(props)
}
public render(): React.ReactElement<IVoteListProps> {
// const { vote } = this.state;
return (
<VoteDetails voteDetails={this.props.adminServicePanel.getVotesInfo()} />
)
};
}
public render(): React.ReactElement<IVoteSecurityAppProps> {
return (
<main className="ui main text container">
<VoteList adminServicePanel={this.props.adminPanelService}/>
</main>
);
import {HttpClient} from '#microsoft/sp-http';
import { reject } from 'lodash';
import {IAdminPanelService} from './IAdminPanelService';
import {IReportData} from './IReportData'
import { IVoteDetailsData } from './IVoteDetailsData';
import {IVoteInfo} from './IVoteInfo'
import {VoteOptions} from './VoteOptions';
export class AdminPanelService implements IAdminPanelService {
//////////////////////////////MOCK////////////////////////////////////////////
private voteInfos : IVoteDetailsData[];
private reportData : IReportData[];
//////////////////////////////MOCK////////////////////////////////////////////
constructor(private httpClient: HttpClient, private serverRelativeSiteUrl: string) {
//MOCK
this.voteInfos = [
{
id : 1,
title : "xxx",
voteType : "xx"
},
{
id : 2,
title : "xxx",
voteType : "xxx"
}
];
}
public getVotesInfo () : Promise<IVoteDetailsData[]> {
return new Promise<IVoteDetailsData[]>((resolve : (voteMiniInfo : IVoteDetailsData[]) => void, reject : (error: any) => void): void =>{
resolve(this.voteInfos);
})
}
}
export interface IAdminPanelService {
getVotesInfo:() => Promise<IVoteDetailsData[]>;
}
import * as React from 'react';
import styles from './VoteSecurityApp.module.scss';
import { IVoteSecurityAppProps } from './IVoteSecurityAppProps';
import { escape } from '#microsoft/sp-lodash-subset';
import { VoteList } from './VoteList/VoteList';
export default class VoteSecurityApp extends React.Component<IVoteSecurityAppProps, {}> {
public render(): React.ReactElement<IVoteSecurityAppProps> {
return (
<main className="ui main text container">
<VoteList adminServicePanel={this.props.adminPanelService}/>
</main>
);
}
}
export class VoteDetails extends React.Component<IVoteDetailsProps, IVoteDetailsState>
{
state = {
vote: null,
}
// change this
componentDidMount() {
this.props.voteDetails().then(result => this.setState({
vote: result
}));
};
// rest of your codes here
}
export class VoteList extends React.Component<IVoteListProps, {}> {
constructor(props: IVoteListProps) {
super(props);
console.log(props)
}
public render(): React.ReactElement<IVoteListProps> {
// const { vote } = this.state;
return (
<VoteDetails voteDetails=
{this.props.adminServicePanel.getVotesInfo} /> // change this line
)
};
}
All errors "TypeError: Cannot read property '......' of null" in spfx components, when you call
this.state.{varname}
or
this.props.{varname}
solves one of:
Add binding 'this' in constructor to method where rise error
this.{methodname} = this.{methodname}.bind(this)
You miss initialize state in constructor (for React.Component<props,state>)
this.state = {};
You use value from props or state and miss check it for null
In question I see all of this things. For example, in this peice of code state will not be initialized, otherwise component has state
export class VoteDetails extends React.Component<IVoteDetailsProps, IVoteDetailsState>{
constructor(props: IVoteListProps) {
super(props);
console.log(props)
//this.state == null - true
}
}
Second problem is this code
<VoteDetails voteDetails={this.props.adminServicePanel.getVotesInfo()}
getVotesInfo- return promise, not data. This bad practice, use state to hold data, for example
constructor(props: ...){
super(props);
this.state{
data: null
};
this._getData = this._getData.bind(this);
}
componentDidMount(){
this._getData();
}
async _getData(){
if(this.props.adminServicePanel){
let data = await this.props.adminServicePanel.getVotesInfo();
this.setStae({data});
}
}
render():...{
const data = this.state.data;
return(
{data && data.map(...)}
);
}

Reactjs this.props.map is not a function

I'm trying to make a simple message app that takes the users name, and a message, then the messages are passed down and displayed in another component. In the component that should display the messages I'm getting an error saying this.props.messages.map is not a function.
Here is my code sandbox:
https://codesandbox.io/s/unruffled-pasteur-nz32o
And here is my actual code:
Parent component:
import React, { Component } from "react";
import Messages from "./Messages";
import Input from "./Input";
export default class Container extends Component {
constructor(props) {
super(props);
this.state = {
messages: {
user: [],
message: []
}
};
}
updateMessage(message, sender) {
this.setState({
messages: [...this.state.messages, { user: sender, message: message }]
});
}
render() {
return (
<div>
<Messages messages={this.state.messages} />
<Input
updateMessage={(message, sender) =>
this.updateMessage(message, sender)
}
/>
</div>
);
}
}
And here is where the messages should be displayed (and also where I am getting the error):
import React, { Component } from "react";
export default class Messages extends Component {
render() {
return this.props.messages.map(message => {
return (
<div>
{message.user}: {message.maessage}
</div>
);
});
}
}
Any ideas what could be causing my error? Thanks!
messages is initialized as an object in state. If you want to map over it, it should be an array. So rather than this:
constructor(props) {
super(props);
this.state = {
messages: {
user: [],
message: []
}
};
}
You'll, want this:
constructor(props) {
super(props);
this.state = {
messages: [
{
user: [],
message: []
}
]
};
}

Is passing the _setTitle method with the props the way to go?

want to update the title present in the Header
everytime we navigate to a new page.
Is passing the _setTitle method with the props the way to go?
import React, { Component } from 'react';
import PropTypes from 'prop-types';
Main App component
export class App extends Component {
constructor() {
super();
this.state = { pageTitle: 'My app' };
this._setTitle = this._setTitle.bind(this);
}
_setTitle(title) {
this.setState({ pageTitle: title });
}
render() {
const { pageTitle } = this.state.pageTitle;
return (
<div>
<Header title={pageTitle} />
{React.cloneElement(children, { setTitle: this._setTitle })}
<Footer />
</div>
);
}
}
Header and Footer Components
export class Header extends Component {
static propTypes = {
title: PropTypes.string
};
// ...
render() {
const { title } = this.props;
return <h2>{title}</h2>;
}
}
export class Footer extends Component {
// Footer code
}
Following are the different page Components:
export class Profile extends Component {
static propTypes = {
setTitle: PropTypes.func.isRequired
};
componentWillMount() {
this.props.setTitle('Profile');
}
}
export class Projects extends Component {
static propTypes = {
setTitle: PropTypes.func.isRequired
};
componentWillMount() {
this.props.setTitle('Projects');
}
// ...
}
export class ProjectForm extends Component {
static propTypes = {
setTitle: PropTypes.func.isRequired
};
componentWillMount() {
this.props.setTitle('New Project');
}
// ...
}
export class Translators extends Component {
static propTypes = {
setTitle: PropTypes.func.isRequired
};
componentWillMount() {
this.props.setTitle('Translators');
}
// ...
}
// ...
How can I improve upon this. I'm new to react so pls suggest If you have any ideas, I'll implement it. Thank you.
You can make use of Context and pass the setTitle method as a Context value, then you can create a Component that has the logic of setting the Title, A simple implementation would look like
const TitleContext = React.createContext();
export class App extends Component {
constructor() {
super();
this.state = { pageTitle: 'My app' };
this._setTitle = this._setTitle.bind(this);
}
_setTitle(title) {
this.setState({ pageTitle: title });
}
render() {
const { pageTitle } = this.state.pageTitle;
return (
<TitleContext.Provider value={{setTitle: this._setTitle}}
<div>
<Header title={pageTitle} />
{this.props.children}
<Footer />
</div>
</TitleContext.Provider>
);
}
}
class TitleSetter extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired
}
componentDidMount() {
this.context.setTitle(this.props.title)
}
}
TitleSetter.contextTypes = TitleContext;
Now in any component you can simply render the TitleSetter like
export class Profile extends Component {
render() {
return (
<div>
<TitleSetter title="Profile" />
{/* other context */}
</div>
)
}
}
Also while looking into context, please look at the this question on how to access context outside of render

Call child component function from parent

How do I call a child component function from the parent component? I've tried using refs but I can't get it to work. I get errors like, Cannot read property 'handleFilterByClass' of undefined.
Path: Parent Component
export default class StudentPage extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
newStudentUserCreated() {
console.log('newStudentUserCreated1');
this.refs.studentTable.handleTableUpdate();
}
render() {
return (
<div>
<StudentTable
studentUserProfiles={this.props.studentUserProfiles}
ref={this.studentTable}
/>
</div>
);
}
}
Path: StudentTable
export default class StudentTable extends React.Component {
constructor(props) {
super(props);
this.state = {
studentUserProfiles: props.studentUserProfiles,
};
this.handleTableUpdate = this.handleTableUpdate.bind(this);
}
handleTableUpdate = () => (event) => {
// Do stuff
}
render() {
return (
<div>
// stuff
</div>
);
}
}
UPDATE
Path StudentContainer
export default StudentContainer = withTracker(() => {
const addStudentContainerHandle = Meteor.subscribe('companyAdmin.addStudentContainer.userProfiles');
const loadingaddStudentContainerHandle = !addStudentContainerHandle.ready();
const studentUserProfiles = UserProfiles.find({ student: { $exists: true } }, { sort: { lastName: 1, firstName: 1 } }).fetch();
const studentUserProfilesExist = !loadingaddStudentContainerHandle && !!studentUserProfiles;
return {
studentUserProfiles: studentUserProfilesExist ? studentUserProfiles : [],
};
})(StudentPage);
My design here is: component (Child 1) creates a new studentProfile. Parent component is notified ... which then tells component (Child 2) to run a function to update the state of the table data.
I'm paraphrasing the OP's comment here but it seems the basic idea is for a child component to update a sibling child.
One solution is to use refs.
In this solution we have the Parent pass a function to ChildOne via props. When ChildOne calls this function the Parent then via a ref calls ChildTwo's updateTable function.
Docs: https://reactjs.org/docs/refs-and-the-dom.html
Demo (open console to view result): https://codesandbox.io/s/9102103xjo
class Parent extends React.Component {
constructor(props) {
super(props);
this.childTwo = React.createRef();
}
newUserCreated = () => {
this.childTwo.current.updateTable();
};
render() {
return (
<div className="App">
<ChildOne newUserCreated={this.newUserCreated} />
<ChildTwo ref={this.childTwo} />
</div>
);
}
}
class ChildOne extends React.Component {
handleSubmit = () => {
this.props.newUserCreated();
};
render() {
return <button onClick={this.handleSubmit}>Submit</button>;
}
}
class ChildTwo extends React.Component {
updateTable() {
console.log("Update Table");
}
render() {
return <div />;
}
}

Resources