Misunderstanding React. FC and class component interaction - reactjs

I'am new to React and don't understand something. I have two components: parent functional and child class component. Suppose, from FC I need to pass very simple array from useState hook. And when in class component in constructor I try to log out props - the result is empty array. But in component render array items successfully. If I don't use - useState hook and pass existing array - console.log(props) results that array. Why it happens? I think it's associated with async operations, but what if need to define initial state in constructor based on props? Since props undefined , I can't do it. Сan you help me figure it out
const FunctionalComponent = () => {
const [state, setState] = useState([]);
useEffect(() => {
setState(['Jim', 'Bob', 'Tom']);
}, []);
return (
<div>
<ClassComponent names={state} />
</div>
);
}
// And class component
class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
// Some state ...
};
console.log(props); --- // []
}
render() {
const { names } = this.props;
return <div>{names}</div>; -- // renders items
}
}
https://codesandbox.io/s/props-to-class-component-from-usestate-5z5k3?file=/src/index.js

You can see how the class component renders proper data but console.log output does not reffer to them. That's because creating and changing state is async operation. To see how props changed you must use useEffect for FC and componentDidUpdate for class components.
class Counter extends React.Component {
constructor(props) {
super(props);
}
componentDidUpdate() {
console.log(this.props.names)
}
render() {
const { names } = this.props;
return <div>{names}</div>;
}
}
Or you can put your console.log right inside of the render section:
render() {
console.log(this.props.names)
const { names } = this.props;
return <div>{names}</div>;
}
If you want to console.log new state of class component, use callback function as the second parameter of setState like shown below:
setState({counter: this.counter + 1}, ()=>console.log(counter))
Otherwise, it will show you the previous state value.

Your initial state is [] as defined by useState. With this state the functional component is initially rendered and it also renders ClassComponent with [] as the initial props. That's what is passed to constructor.
After that the constructor is no longer called. Instead, the new props will be only assigned to the component.

Related

useState with arrays not rerendering

I am facing issue while using useState hook with array. I checked various resources on stackoverflow, but could not fix it.
my basic code snippet looks like :
const [users, setUsers] = useState([]);
function addNewContact(user) {
const newUsers = [...users,user];
console.log(newUsers);
setUsers(newUsers);
}
<CardContainer users={users}></CardContainer>
class CardContainer extends Component {
constructor(props) {
super(props);
console.log("this -> ");
console.log(this.props.users);
this.state = {
users: this.props.users
}
}
render() {
//console.log(this.state.users)
return (
<div class="row row-cols-1 row-cols-md-2 g-4">
{
this.state.users.map(user => {
return <Card id={user.phone} title={user.name} email={user.email} phone={user.phone}></Card>
})
}
</div>
)
}
}
export default CardContainer;
I am able to see updated array in the console, but the component using it is not rendering again. Can anyone please help me on this.
The issue is due to you're storing the prop in the state of the child component, which is assigned on component initialization and component initialization/constructor only run one, until its remounted. After that, whenever, the state changes in the parent component, the child component is not re-rendering, because it uses its own state for map.
This below code only runs once on the component initialization.
this.state = {
users: this.props.users
}
In the child component, you can directly use the props and the child component will always re-render on change in the parent component. Instead of this.state.users.map you can directly map the array from props like this this.props.users.map. This way,the component will re-render on state change in the parent compoenent.
As #Junaid said, constructor is only called once before component mounting. If you really need to set a separate state inside the child component, then you can use componentDidUpdate(prevProps) react life cycle method. Make sure to compare previous and current props in order to avoid infinite loop of re-rendering.
componentDidUpdate(prevProps) {
if (this.props.users !== prevProps.users) {
this.setState({ users: this.props.users });
}
};

How to update the value of a prop upon a state change in React

I have a parent React component (MainComponent) that renders a child component (theKeyComponent) and passes a constant as a prop (myID). The parent component also tracks the state 'activeLink'.
import theKeyComponent from "../components/theKeyComponent.jsx";
export default class MainComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
activeLink: '#section1'
}
}
render(){
const myId = this.state.activeLink === '#section1' ? '0001' : '0002';
return(){
<theKeyComponent myID={myID} />
<otherComponent otherPropsHere={otherProps} />
}
}
}
Here's a bit from inside the KeyComponent
export default class HelpScoutBeacon extends React.Component {
static propTypes = {
myID: PropTypes.string.isRequired
}
componentDidMount() {
myAPImethod('init', this.props.myID);
}
render(){return(){}}
}
I want to change the value of the constant myID depending on the value of 'activeLink'. This is not a problem when both components are mounted for the first time. However, when the value of 'activeLink' changes 'myID' doesn't change since the child component is already mounted.
I'm struggling to see what would be the 'React way' of doing this. Should 'myID' be set as another state and the function that sets the state for activeLink should include another one to set the state of myID? Or is this overcomplicating things and there's an easier way to re-render only that particular child component so that it considers the new value of myID.
I'm new to React so I was hoping I could get some clarification form SO community.
This is not a problem when both components are mounted for the first
time. However, when the value of 'activeLink' changes 'myID' doesn't
change since the child component is already mounted.
The issue is with regards to how you handle the trigger of the API call.
componentDidMount will only trigger when the component was initially mounted. This will not be triggered if a state is updated. You are going to want to use componentDidUpdate React Lifecycle as well in augmentation to componentDidMount. componentDidUpdate will trigger when your activeLink state changes because you pass it as props to theKeyComponent
componentDidUpdate() {
myAPImethod('init', this.props.myID);
}
Reference: https://reactjs.org/docs/react-component.html#componentdidupdate
import theKeyComponent from "../components/theKeyComponent.jsx";
export default class MainComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
activeLink: '#section1'
}
}
static getDerivedStateFromProps(props, state) {
if (props.activeLink !== state.activeLink ) {
// updating the state here if there are any changes in props.
return {
activeLink: props.activeLink,
};
}
// Return null if the props hasn't changed
return null;
}
use getDerivedStateFromProps hook to find out changes of props. you need to update the changes of props in state.
render(){
// now, here your state is updated.
const myId = this.state.activeLink === '#section1' ? '0001' : '0002';
return (
<theKeyComponent myID={myID} />
<otherComponent otherPropsHere={otherProps} />
)
}
Any suggestions are welcome.

Trouble with race condition in component lifecycle methods - How to get data in time?

I've tried a number of configurations to solve this issue, but basically I'd like parent component to run an async function, morph the result, and pass that as props to my child. I'm not really sure where to incorporate lifecycle methods, namely because ComponentDidMount will run, set state, and re-render the parent component just fine and a console.log of state inside the parent render method shows the state updated correctly with my fields. But the child component renders and shows an empty array in props.
Any ideas on this one?
import * as React from 'react';
interface CustomState {
fields: any;
}
class Parent extends React.Component<{}, CustomState> {
constructor(props) {
super(props);
this.state = {
fields: [],
};
}
public async componentDidMount() {
let fields = await asyncCall();
const viewFields = this.buildViewFields(fields);
this.setState({
fields: viewFields,
});
}
private buildViewFields(fields) {
const viewFields = fields.map((f) => {
return {
name: f.Title,
};
});
return viewFields;
}
public render() {
// ISSUE: this.state.fields if logged here shows viewfields
return <ChildComponent fields={this.state.fields}></ChildComponent>;
}
}
export default Parent;
Here's my child component:
class ChildComponent extends React.Component<Props, State> {
constructor(props) {
super(props);
this.state = {
viewFields: this.props.fields,
};
}
public render() {
console.log(this.state); // DOES NOT HAVE VIEWFIELDS
return (
<GrandChildComponent
fields={this.state.viewFields}
/>
);
}
}
export default ChildComponent;
You don't need to do this part
this.state = {
viewFields: this.props.fields,
};
You can just use props.fields right away inside your ChildComponent.
What happens right now is:
Parent renders, fields is empty
Child renders, it sets viewFields to empty fields array inside constructor
Parent fetches actual fields and calls setState with them, rerenders self and Child
Child rerenders, but constructor is not invoked again.
If you want to actually setState inside Child from Parent props you need to use lifecycle method componentDidUpdate. It is rarely needed though and usually is just bad practice.

Async update parent props to child component

How to send updated props of parent component to child component after async operation. Is there any lifecycle method available
class SignUp extends Component {
constructor(props){
super(props);
this.state = {...props};
}
componentDidMount(){
this.props.UserSignupType();
}
render(){
<Inputs {...this.state} >
}
}
const stateToProps = state => ({
signupInputs: state.signupInputs
});
connect(stateToProps, null)(SignUp);
class Inputs extends Component {
constructor(props){
super(props);
this.state = {...props};
}
render(){
if(!this.state.isLoaded) {
return <Loading />
} else {
let inputs = [];
this.state.inputs.forEach(function(input){
inputs.push(<TextField value={input.text}>)
});
return <View>{inputs}</View>
}
}
}
Note: Possible duplicate of Pass props to child components after Async update
First correction: You do not need to add data in state from props if you are not updating it. Also if you are updating it, it should be in state of the child component. So parent component can directly pass props to child component like this:
<Inputs {...this.props} >
Now in Inputs component you can directly use props if it's not going to change or you can add it to state using componentWillReceiveProps function as shown below:
componentWillReceiveProps(nextProps){
this.setState({...nextProps});
}
The state can be updated via getDerivedStateFromProps.
Its a static method, where you can compare the state and the new props and update the state.
static getDerivedStateFromProps(nextProps, prevState) {
// Store myId in state.
if (nextProps.myId !== prevState.myId) {
// this updates the state
return {
myId: nextProps.myId
};
}
// This implies it won't update the state
return null;
}
Why do you need to set the state of the child component to the props received? I would just use the props directly.
In your case I am not sure that the constructor gets called on refresh, thus the state of the child component does not get updated. Try using the props directly.

React / Redux Components not re-rendering on state change

I think this question has been answer several time but I can't find my specific case.
https://codesandbox.io/s/jjy9l3003
So basically I have an App component that trigger an action that change a state call "isSmall" to true if the screen is resized and less than 500px (and false if it is higher)
class App extends React.Component {
...
resizeHandeler(e) {
const { window, dispatch } = this.props;
if (window.innerWidth < 500 && !this.state.isSmall) {
dispatch(isSmallAction(true));
this.setState({ isSmall: true });
} else if (window.innerWidth >= 500 && this.state.isSmall) {
dispatch(isSmallAction(false));
console.log(isSmallAction(false));
this.setState({ isSmall: false })
}
};
componentDidMount() {
const { window } = this.props;
window.addEventListener('resize', this.resizeHandeler.bind(this));
}
...
I have an other component called HeaderContainer who is a child of App and connected to the Store and the state "isSmall", I want this component to rerender when the "isSmall" change state... but it is not
class Header extends React.Component {
constructor(props) {
super(props);
this.isSmall = props.isSmall;
this.isHome = props.isHome;
}
...
render() {
return (
<div>
{
this.isSmall
?
(<div>Is small</div>)
:
(<div>is BIG</div>)
}
</div>
);
}
...
even if I can see through the console that redux is actually updating the store the Header component is not re-rendering.
Can someone point out what I am missing ?
Am I misunderstanding the "connect()" redux-react function ?
Looking at your code on the link you posted your component is connected to the redux store via connect
const mapStateToProps = (state, ownProps) => {
return {
isHome: ownProps.isHome,
isSmall: state.get('isSmall')
}
}
export const HeaderContainer = connect(mapStateToProps)(Header);
That means that the props you are accessing in your mapStateToProps function (isHome and isSmall) are taken from the redux store and passed as props into your components.
To have React re-render your component you have to use 'this.props' inside the render function (as render is called every time a prop change):
render() {
return (
<div>
{
this.props.isSmall
?
(<div>Is small</div>)
:
(<div>is BIG</div>)
}
</div>
);
}
You are doing it well in the constructor but the constructor is only called once before the component is mounted. You should have a look at react lifecycle methods: https://reactjs.org/docs/react-component.html#constructor
You could remove entirely the constructor in your Header.js file.
You should also avoid using public class properties (e.g. this.isSmall = props.isSmall; ) in react when possible and make use of the React local state when your component needs it: https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-class
A component is only mounted once and then only being updated by getting passed new props. You constructor is therefore only being called once before mount. That means that the instance properties you set there will never change during the lifetime of your mounted component. You have to directly Access this.props in your render() function to make updating work. You can remove the constructor as he doesn't do anything useful in this case.

Resources