I use React 16.3.1 and next.js.
And I put getDerivedStateFromProps inside the class extending PureComponent.
Here is the code:
Header.js
import { PureComponent } from 'react'
...
export default class Header extends PureComponent {
constructor (props) {
super(props)
this.colorAnimationProps = {
animationDuration: '0.4s',
animationFillMode: 'forwards'
}
this.colorAnimationStyle = {
toColor: {
animationName: 'toColor',
...this.colorAnimationProps
},
toTransparent: {
animationName: 'toTransparent',
...this.colorAnimationProps
}
}
this.state = {
colorAnimation: {},
headerModal: null
}
}
componentDidMount () {
if (this.props.isColor) {
this.setState({colorAnimation: this.colorAnimationStyle.toColor})
}
}
static getDerivedStateFromProps (nextProps, prevState) {
console.log('should go here')
if (nextProps.isColor) {
return {colorAnimation: this.colorAnimationStyle.toColor}
}
return {colorAnimation: this.colorAnimationStyle.toTransparent}
}
render () {
...
}
}
And here is the parent that modifies the prop:
index.js
import { PureComponent } from 'react'
...
import Header from '../components/Header'
import Layout from '../components/Layout'
import { withReduxSaga } from '../redux/store'
class Index extends PureComponent {
constructor (props) {
super(props)
this.state = {
isHeaderColor: false
}
}
componentDidMount () {
if (window.pageYOffset > 50) {
this.setState({isHeaderColor: true})
}
window.addEventListener('scroll', (e) => {
if (window.pageYOffset > 50) {
this.setState({isHeaderColor: true})
} else {
this.setState({isHeaderColor: false})
}
})
}
render () {
return (
<Layout url={this.props.url}>
<Header isColor={this.state.isHeaderColor} />
...
</Layout>
)
}
}
export default withReduxSaga(Index)
My problem is: getDerivedStateFromProps is not called when the prop changes. At least, it should do console.log, but it doesn't.
Can anybody here help me?
make sure you have the right versions of both react and react-dom in your package.json:
"react": "^16.3.1",
"react-dom": "^16.3.1"
I see that support for this hook was patched in version - 6.0.0-canary.2 of next.JS. So I'm guessing you use an older version.
Related
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.
So I'm new to react and I'm trying to use a boilerplate but I'm getting the following error in the chrome console. Sorry if this is a repeat question but I've tried to google it and found nothing. Thanks for the help in advance.
(Console)
TypeError: this._modal.$el.on is not a function
(index.js)
import React from 'react'
import UIkit from 'uikit'
export default class Modal extends React.Component {
constructor(props) {
super(props)
}
componentDidMount() {
this._modal = UIkit.modal(this.refs.modal, {
container: false,
center: true
// stack: true
})
console.log(this._modal)
this._modal.$el.on('hide', () => {
this.props.onHide()
})
this._modal._callReady()
if(this.props.visible) {
this._modal.show()
}
}
componentWillReceiveProps(nextProps) {
const { visible } = nextProps
if(this.props.visible !== visible) {
if(visible) {
this._modal.show()
} else {
this._modal.hide()
}
}
}
render() {
return (
<div ref="modal">
{this.props.children}
</div>
)
}
}
It is recommended to handle your modal with your compoenent state and refs are usually used for DOM manipulation.
What you can do is to initialize your state:
constructor(props) {
super(props)
this.state= {
isOpen : false
}
}
in your componentWillReceiveProps:
componentWillReceiveProps(nextProps) {
const { visible } = nextProps
if(this.props.visible !== visible) {
this.setState({
isOpen: visible
})
}
}
and in your render method:
render() {
return (
<div ref="modal">
{this.state.isOpen && this.props.children}
</div>
)
}
Let me know if this issue still persists. Happy to help
I have a component which renders if its prop.paramA is different from paramA value stored in cookie.
Following is my attempt to move the cookie management logic into a separate component. The problem is that the question component gets rendered only once when showQuestion is false so I never get to see the question.
Currently, my code looks like below
// question.js
import React from 'react';
import ToggleDisplay from 'react-toggle-display';
import {withCookies, Cookies} from 'react-cookie';
class Question extends React.Component {
static get propTypes() {
return {
cookies: instanceOf(Cookies).isRequired
}
}
constructor(props) {
super(props);
this.state = {
paramA: props.paramA,
showQuestion: props.showQuestion
};
}
componentDidMount() {
if (this.state.showQuestion) {
// do some ajax calls etc.
}
}
render() {
return (
<div id="simplequestion">
<ToggleDisplay show={this.state.showQuestion}>
<div>What is your SO profile?</div>
</ToggleDisplay>
</div>
);
}
}
export default withCookies(Question);
I wrote following CookieManager component -
// cookiemanager.js
import React from 'react';
import {withCookies, Cookies} from 'react-cookie';
class CookieManager extends React.Component {
static get propTypes() {
return {
cookies: instanceOf(Cookies).isRequired
}
}
constructor(props) {
super(props);
let propA = props.cookies.get('propA');
if (!propA || propA !== props.propA) {
props.cookies.set('propA', props.propA, { path: '/', maxAge: 604800});
console.log('changed', propA, props.propA);
props.propAChanged(true);
} else if (propA === props.propA) {
console.log('unchanged', propA, props.propA);
props.propAChanged(false);
}
}
render() {
return <div id="cookieManager"></div>;
}
}
export default withCookies(CookieManager);
Following is the top level app
// app.js
import React from 'react';
import Question from './question';
import CookieManager from './cookiemanager';
import { CookiesProvider } from 'react-cookie';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
paramA: props.paramA,
showQuestion: false,
avoidUpdate: false
};
this.updateShowQuestion = this.updateShowQuestion.bind(this);
}
updateShowQuestion(x) {
if (this.state.avoidUpdate) {
return;
}
this.setState({
paramA: this.state.paramA,
showQuestion: x,
avoidUpdate: !this.state.avoidUpdate
});
}
componentWillUpdate(nextProps, nextState) {
if (!nextState.avoidUpdate && nextState.paramA === this.state.paramA) {
this.setState({
paramA: this.state.paramA,
showQuestion: this.state.showQuestion,
avoidUpdate: true
});
}
}
render() {
return (
<CookiesProvider>
<CookieManager paramA={this.state.paramA} ridChanged={this.updateShowQuestion}>
<Question
paramA={this.state.paramA}
showQuestion={this.state.showQuestion}/>
</CookieManager>
</CookiesProvider>
);
}
}
export default App;
You're reading from state here:
<ToggleDisplay show={this.state.showQuestion}>
But showQuestion is set only once, in the constructor:
this.state = {
paramA: props.paramA,
showQuestion: props.showQuestion
};
When this.props changes your component is rendered, but you're still reading from this.state which is not updated, so the visible output is the same.
I see two ways you can solve this:
Update this.state.showQuestion whenever the props change:
static getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.showQuestion !== prevState.showQuestion)
return { showQuestion };
}
In render(), read from this.props.showQuestion:
<ToggleDisplay show={this.props.showQuestion}>
This way may look easier but with the other you gain more fine-grained control over state changes.
Another thing you've missed is found here:
componentWillUpdate(nextProps, nextState) {
if (!nextState.avoidUpdate && nextState.paramA === this.state.paramA) {
this.setState({
paramA: this.state.paramA,
showQuestion: this.state.showQuestion, // this line
avoidUpdate: true
});
}
}
You're receiving new props, but you're setting the old state: showQuestion: this.state.showQuestion. Instead, set the new showQuestion value from the new props - nextProps, this way:
componentWillUpdate(nextProps, nextState) {
if (!nextState.avoidUpdate && nextState.paramA === this.state.paramA) {
this.setState({
paramA: this.state.paramA,
showQuestion: nextProps.showQuestion, // this line
avoidUpdate: true
});
}
}
import { Mongo } from 'meteor/mongo';
import { Meteor } from 'meteor/meteor';
import React, {Component} from 'react';
import {check} from 'meteor/check';
export const Adressen = new Mongo.Collection('Phonebook');
if (Meteor.isServer) {
Meteor.publish('ArrayToExport', function(branches) {
check(branches, [Match.Any]);
if(branches.length > 10){
return this.ready()
};
return Adressen.find(
{branche: {$in: branches}}, {fields: {firmenname:1, plz:1}}
);
});
}
.
import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import {Adressen} from "../api/MongoDB";
class ExportArray extends Component{
constructor(props){
super(props);
this.state = {
branches: this.props.filteredBranches
};
}
render(){
return(
<div>
<button onClick={this.exportArrays}></button>+
</div>
);
}
}
export default withTracker( (branches) => {
Meteor.subscribe('ArrayToExport', branches);
return {
ArrayToExport: Adressen.find({}).fetch()
};
})(ExportArray);
this.props.filteredBranche is a pure array,generated through controlled input field. this.props.filteredBranches changes as Input changes, in parent Component.
I thought I was sending my this.props.filteredBranches as an argument through withTracker function. But nothing is passed to the publish function.
if (Meteor.isServer) {
arrayExfct = function (array){
return {
find: {branche:{$in: array }},
fields: {firmenname:1, plz:1}
};
}
Meteor.publish('ArrayToExport', function (array) {
return Adressen.find(
arrayExfct(array).find, arrayExfct(array).fields);
});
}
.
export default withTracker( () => {
arrayExfct = function(array) {
return {
find: {branche: {$in: array}},
fields: {firmenname:1, plz:1}
}
}
var array = ['10555'];
Meteor.subscribe('ArrayToExport', array );
var arrayExfct = Adressen.find(arrayExfct(array).find, arrayExfct(array).fields);
return {
ArrayToExport: Adressen.find({}).fetch()
};
})(ExportArray);
It would help if you also added an example of where you used this component and how you pass props to it, but I think I see your problem.
You expect the local state in your rendering component to get into the withTracker container, but that would be the other way around. When you make the withTracker container, you are really making another react component that renders your display component (ExportArray) and passes the data (ArrayToExport) down into it.
So, props go like this currently:
external render -> withTracker component -> ExportArray
What you need to do it to get the filteredBranches (which you pass from a parent component?) from the props argument in withTracker and pass that to the subscribtion,
class ExportArray extends Component{
exportArrays () {
const { ArrayToExport } = this.props;
}
render(){
const { ArrayToExport } = this.props;
return(
<div>
<button onClick={this.exportArrays}></button>+
</div>
);
}
}
export default withTracker(propsFromParent => {
const { filteredBranches } = propsFromParent;
Meteor.subscribe('ArrayToExport', filteredBranches);
return {
ArrayToExport: Adressen.find({}).fetch()
};
})(ExportArray);
Hi the issue is with the code below. The parameter called branches is the props so branches.branches is the array you passed in.
export default withTracker( (branches) => {
Meteor.subscribe('ArrayToExport', branches);
return {
ArrayToExport: Adressen.find({}).fetch()
};
})(ExportArray);
Try the following.
export default withTracker( ({branches}) => {
Meteor.subscribe('ArrayToExport', branches);
return {
ArrayToExport: Adressen.find({}).fetch()
};
})(ExportArray);
Notice all that changed was
(branches)
became
({branches})
I solved my problem with a combination of Session Variables and State.
//Client
import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import {Adressen} from "../api/MongoDB";
import {Meteor} from 'meteor/meteor';
import { Session } from 'meteor/session';
class ExportArray extends Component{
constructor(){
super();
this.state = {
x: [],
y: []
};
this.exportArrays = this.exportArrays.bind(this);
}
exportArrays(e){
e.preventDefault();
this.setState({x: this.props.filteredBranches});
this.setState({y: this.props.filteredPostleitzahlen});
}
render(){
var selector = {branche: {$in: this.state.x},plz: {$in: this.state.y}};
Session.set('selector', selector);
return(
<div>
<button onClick={this.exportArrays}> Commit </button>
</div>
);
}
}
export default withTracker( () => {
const ArrayfürExport = Meteor.subscribe('ArrayToExport', Session.get('selector') );
return {
ArrayToExport: Adressen.find({}).fetch()
};
})(ExportArray);
//Server
Meteor.publish('ArrayToExport', function (selector) {
console.log('von mongodb', selector);
return Adressen.find(
selector
, {
fields: {firmenname:1, plz:1}
});
});
}
The function renderNotes() is supposed to return the mapped array which is in a separate file. I noticed that when I return some JSX nothing returns to the screen. I think I know the reason why is because it is returning the info to the tracker function. How would I get the info to return to the renderNotes() function while inside the tracker.autorun() function?
import { Meteor } from "meteor/meteor";
import React from "react";
import { withRouter, Link } from "react-router-dom";
import { Accounts } from "meteor/accounts-base";
import { Tracker } from "meteor/tracker";
import SubjectRoutes from "./subjectRoutes/subjectRoutes";
import { Notes } from "../methods/methods"
import Menu from "./Menu.js";
class Home extends React.Component{
componentWillMount() {
if(!Meteor.userId()){
this.props.history.replace("/login")
}
}
logoutUser(e){
e.preventDefault()
Accounts.logout(() => {
this.props.history.push("/login");
});
}
renderNotes(){
Tracker.autorun(function () {
Meteor.subscribe('notes');
let notes = Notes.find().fetch();
// return notes.map((note) => {
// return <p>{note.imageURL}</p>
// })
return <p>asdas</p> //<--Here
});
}
render(){
return (
<div>
<button onClick={this.logoutUser.bind(this)}>Logout</button>
{this.renderNotes()}
<Menu />
</div>
)
}
}
export default withRouter(Home);
Don't know whether this is a proper answer but I like to usually do something like this
import TrackerReact from 'meteor/ultimatejs:tracker-react';
import { Notes } from "../methods/methods";
export default class Home extends TrackerReact(React.Component) {
constructor(props,) {
super(props);
this.state = {
subscription:{
publishNotes: Meteor.subscribe("publish-Notes")
}
};
}
returnNotes(){
return Notes.find().fetch();
}
render(){
...
const stuff = this.returnNotes().map((note)=>{
return <p>{note}</p>
});
return (
....
{stuff}
)
}
}
This worked for me:
import { Meteor } from "meteor/meteor";
import React from "react";
import { withRouter, Link } from "react-router-dom";
import { Accounts } from "meteor/accounts-base";
import { Tracker } from "meteor/tracker";
import SubjectRoutes from "./subjectRoutes/subjectRoutes";
import { Notes } from "../methods/methods"
import Menu from "./Menu.js";
class Home extends React.Component{
constructor(props){
super(props)
this.state = {
notes: []
}
}
componentWillMount() {
if(!Meteor.userId()){
this.props.history.replace("/login")
}
this.tracker = Tracker.autorun(()=>{
Meteor.subscribe('notes');
let notes = Notes.find().fetch();
this.setState({ notes })
});
}
componentWillUnmount() {
this.tracker.stop();
}
logoutUser(e){
e.preventDefault()
Accounts.logout(() => {
this.props.history.push("/login");
});
}
renderNotes(notes){
return notes.map((note) => {
return (
<div key={note._id}>
<img src={note.imageURL} />
<p>{note.type}</p>
</div>
)
});
}
render(){
return (
<div>
<button onClick={this.logoutUser.bind(this)}>Logout</button>
<Menu />
{this.renderNotes(this.state.notes)}
</div>
)
}
}
export default withRouter(Home);