How to fix: setState does not work in gastby? - reactjs

I'm using gatsby for server side rendering.
Here's my code:
class BookSearch extends Component {
state = {
search: '',
books: '',
};
componentDidMount() {
this.loadData()
}
loadData () {
axios.get('/books/list')
.then(response => {
this.setState({books: response.data.books});
console.dir(response.data.books);
})
.catch(error => {
this.setState({error: true});
});
}
Unfortunately, this.setState does not work in gatsby. componentDidMount is not being called when I load the page. What should I do?

I think the issue is of binding this to loadData method.
You can bind this in 2 ways.
Bind this in the constructor,
constructor(props){
super(props)
this.state = {
search: '',
books: '',
}
this.loadData = this.loadData.bind(this) //Bind this here
}
Or you can simply use arrow function,
loadData = () => { //Arrow function auto binds `this`
axios.get('/books/list')
.then(response => {
this.setState({
books: response.data.books
});
console.dir(response.data.books);
})
.catch(error => {
this.setState({error: true});
});
}

I think you should have got an error? It's because you have not initialized error state. You must initialize state before you can use them:
state = {
search: '',
books: '',
error: false
};
I hope this may fix the issue. Otherwise, I couldn't see any issue in your code.

You mentioned you're using SSR?
Try using componentWillMount in this case, since componentDidMount is not called in SSR.
In case you're using react version > 16.3:
When supporting server rendering, it’s currently necessary to provide the data synchronously – componentWillMount was often used for this purpose but the constructor can be used as a replacement. The upcoming suspense APIs will make async data fetching cleanly possible for both client and server rendering.
Reference: https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html#fetching-external-data
In your case, I think it would make more sense to use the getInitialProps static method. (https://nextjs.org/learn/basics/fetching-data-for-pages/fetching-batman-shows)
If you're not very familiar with SSR, Next.js has great tutorials:
https://nextjs.org/learn/basics/getting-started
This may help you out!

Related

Can't set state in react to array of strings

I'm new to react and having a super hard time. My most recent problem is trying to set the state of 'favMovies' to an array of strings (movie IDs).
States:
export class MainView extends React.Component {
constructor() {
super();
this.state = {
movies: [],
favMovies: [],
user: null,
};
}
Setting states:
onLoggedIn(authData) {
console.log(authData);
console.log(authData.user.FavoriteMovies);
this.setState({
favMovies: authData.user.FavoriteMovies,
});
this.setState({ user: authData.user });
localStorage.setItem('token', authData.token);
localStorage.setItem('user', JSON.stringify(authData.user));
this.getMovies(authData.token);
this.getUsers();
}
I kind of understand that set state is async and doesn't happen until the next render. The part that I'm confused by is the 'user' that get's set after 'favMovies' works as expected, but 'favMovies' is undefined.
I know this is probly a dumb question, but I'm absolutely lost in react right now and struggling. Any help would be appreciated.
It's alaways better to use single setState if possible, because everytime you call setState it will re-render the view. One more tip for you, when you are dealing with object and arrays try to use spread operstor to assign to the state, instead of direct assignment.
onLoggedIn(authData) {
console.log(authData);
console.log(authData.user.FavoriteMovies);
this.setState({
favMovies: [...authData.user.FavoriteMovies],
user: authData.user
});
//this.setState({ user: authData.user });
localStorage.setItem('token', authData.token);
localStorage.setItem('user', JSON.stringify(authData.user));
this.getMovies(authData.token);
this.getUsers();
}
As we know react setState is asynchronous, state won't reflect immediately. We can use callback with setState where we can access updated state.
this.setState(
{
favMovies: [...authData.user.FavoriteMovies],
user: authData.user
// ...
},
() => {
this.doSomethingAfterStateUpdate();
}
);

Best Way to Handle Multiple State Changes

I'm pretty new to react. I'm building a web app that is working pretty well but I'm not sure I'm handling state changes correctly. For instance, I have a method that gets called in componentDidMount.
It's seems inefficient and bad practice to keep calling setState but I'm not really sure of a better way to do it.
I would love some feedback.
class AuditScreen extends Component {
constructor(props) {
super(props);
this.state = {
currentVehicle: null,
currentVehicleIndex: 0,
vehicles: [],
activePictureIndex: 0,
show: false,
loading: false,
next_page: `${props.backend.id}/images`,
};
}
componentDidMount() {
this.getImages()
}
getImages() {
if (!this.state.loading) {
this.setState({
loading: true,
});
this.registerScrollEvent();
axios.get(this.state.next_page)
.then((response) => {
const paginator = response.data;
const vehicles = paginator.data.filter((vehicle) => {
if (vehicle.enhanced && vehicle.enhanced.length) {
return vehicle;
}
});
if (vehicles && vehicles.length) {
this.setState({
vehicles: [...this.state.vehicles, ...vehicles],
next_page: paginator.next_page_url,
});
if (this.state.currentVehicle === null) {
this.setState({
currentVehicle: vehicles[0]
});
}
}
// remove scroll event if next_page_url is null
if (!paginator.next_page_url) {
this.removeScrollEvent();
}
})
.finally((response) => {
this.setState({
loading: false,
});
});
}
}
}
.....
}
Thanks!
Like #Dave Newton says, I don't think this is bad practice. Updating the state doesn't immediately trigger an update. Instead, state updates often (but not always) batch up and then trigger a single update.
This article explains the batching mechanism in detail. Up to React 17, state-update batching only occurs within event handlers and within componentDidMount. They give an explicit example where "setState is called inside componentDidMount which causes only one extra update (and not three)".
So React already does what you want. React 18 is going to give you more control over the batching behavior, and more automatic batching. I found this description helpful for understanding what's coming and also how React 17 and lower currently work.
if you don't want to stop calling the setState method in react, use context for small project or redux to keep states and reducers with functional components. Best regards.

Reactjs - Loading json file in constructor with require

This is the following code
//inside constructor
this.state = {
id: props.id,
name: props.name,
dataLoaded: false,
data: require('../data/'+props.id+'.json')
}
Will the data be available when the component was mounted? or it is async or synchronous?
Is it proper way to load data? Or any better solution available?
Two ways are visible for me. One is if you are only using the 'require' to import a data (static, maybe): I recommend you to checkout Webpack ... and this post. However, if your aim is to load data (dynamic, maybe), it is recommended to start the (maybe time taking) async request by overriding componentDidMount() method. It is not recommended to call any async methods in the constructor. Do the following...
// inside the constructor
this.state = {
id: props.id,
name: props.name,
dataLoaded: false,
data: null
}
componentDidMount () {
// This will only be called once [safe place to start async requests]
const data = require('../data/'+props.id+'.json');
// when it had loaded the json,
this.setState({data});
}

Why setState interrupt componentDidUpdate?

I have this component (simplified version):
export default class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
data: {}
};
}
componentDidUpdate(prevProps, prevState) {
if(this.props.time && this.props.time !== prevProps.time){
this.setState({
isLoading: true
})
fetch(...).then(data => {
this.setState({
data: data
isLoading:false
}
}
}
render(){
{isLoading, data} = this.state;
return (isLoading ? /*show spinner*/ : /* show data*/);
}
}
This component works: it shows a spinner while fetching data, then it shows the data.
I'm trying to test it using jest and enzyme:
test('Mounted correctly', async() => {
let myComponent = mount(<MyComponent time='01-01-18'/>);
myComponent.setProps({time: '02-01-18'}); //necessary to call componentDidUpdate
expect(myComponent.state()).toMatchSnapshot();
}
From my knowledge, in order to call componentDidUpdate you have to call setPros (link). However, following the debugger, the call end when hitting:
this.setState({
isLoading: true
})
Which is kinda of expected, the problem is that the snapshot is:
Object {
"isLoading": true
"data": {}
}
Which is, of course, something that I don't want. How can I solve this?
UPDATE: I found a(n ugly) solution!
The problem is that what we want to test is this setState is completed:
this.setState({
data: data
isLoading:false
}
Now, this doesn't happen even by setting await myComponent.setProps({time: '02-01-18'}); (as suggested in one of the answers), because it doesn't wait for the new asynchronous call created by the setState described above.
The only solution that I found is to pass a callback function to props and call it after setState is completed. The callback function contains the expect that we want!
So this is the final result:
test('Mounted correctly', async() => {
let myComponent = mount(<MyComponent time='01-01-18'/>);
const callBackAfterLastSetStateIsCompleted = () => {
expect(topAsins.state()).toMatchSnapshot();
}
myComponent.setProps({time: '02-01-18', testCallBack: callBackAfterLastSetStateIsCompleted}); //necessary to call componentDidUpdate
expect(myComponent.state()).toMatchSnapshot();
}
And modify the component code as:
this.setState({
data: data
isLoading:false
},this.props.testCallBack);
However, as you can see, I'm modifying a component in production only for testing purpose, which is something very ugly.
Now, my question is: how can I solve this?
All you need to do here to test is make use of async/await like
test('Mounted correctly', async () => {
let myComponent = mount(<MyComponent time='01-01-18'/>);
await myComponent.setProps({time: '02-01-18'}); //necessary to call componentDidUpdate, await used to wait for async action in componentDidUpdate
expect(myComponent.state()).toMatchSnapshot();
}

ReactNative: this.setState Error: null is not an object

React script
class TransactionsList extends Component {
constructor(props) {
super(props);
this.state = {
activeAccountId: "",
accessToken: "",
TransactionsData: "",
};
}
replaceRoute(route, passProps) {
this.props.replaceRoute(route, passProps);
}
async _getToken() {
try {
let accessToken = await AsyncStorage.getItem('AUTH_TOKEN');
if(!accessToken) {
this.replaceRoute('login');
} else {
this.setState({accessToken: accessToken})
}
} catch(error) {
Alert.alert('Print Errorr', error.message)
this.replaceRoute('login');
}
}
componentWillMount(){
this._getToken()
let token = 'Token '+this.state.accessToken
this.load_data(token)
}
render() {
return (
<Container>
// other code
</Container>
)
}
}
Got error in setState in getToken below is catch(error) block output
Print Error null is not an object(evaluating
prevComponentInstance._currentElement)
But same above code works in other screens.
It is not advisable to make api calls in componentWillMount because it is possible that the component will not have been mounted when the api call has finished and you call setState.
Instead, you should make api calls in componentDidMount. According to the documentation:
componentDidMount() is invoked immediately after a component is
mounted. Initialization that requires DOM nodes should go here. If you
need to load data from a remote endpoint, this is a good place to
instantiate the network request. Setting state in this method will
trigger a re-rendering.
And, you also need to bind _getToken as #Jazib mentioned.
You need to bind _getToken method using something like this:
this._getToken().bind(this)
Or you can do this in the constructor for better code (I prefer this one):
constructor(props) {
super(props);
this.state = {
activeAccountId: "",
accessToken: "",
TransactionsData: "",
};
this._getToken() = this._getToken().bind(this)
}
Hope this helps
I know I am replying a bit late but a better way is to use an arrow function instead of using bind on a named function. So you could write your _getToken method like this:
const _getToken = async () => {
// your logic here
}
The arrow function here implicitly assigns the current instance of the component to this keyword whereas in the named function you have to give the this keyword the context by using bind method as mentioned by others.
Also, componentWillMount is now deprecated and its better if you call your method in componentDidMount

Resources