I have this code:
export default class MainStudentPage extends React.Component {
constructor(props) {
super(props);
this.state = {user: {nickname: '', friends: {accepted: [], invites: [], all: []}}};
}
componentWillMount() {
const {uid} = firebase.auth().currentUser;
firebase.database().ref('Users').child(uid).on('value', (r, e) => {
if (e) {
console.log(e);
return null;
}
const user = r.val();
this.setState({user: user});
});
}
render() {
const {user} = this.state;
return (
<LevelSelectComponent user={user}/>
</div>
);
}
}
And this is the child:
export default class LevelSelectComponent extends React.Component {
returnSelect = (user) => {
const lvls = [{
db: 'Podstawówka',
text: 'PODSTAWOWA'
}, {
db: 'Gimnazjum',
text: 'GIMNAZJALNA'
}, {
db: 'Liceum',
text: 'ŚREDNIA'
}];
let options = [];
if (!user.level) {
options.push(<option selected={true} value={null}>WYBIERZ POZIOM</option>)
}
options = options.concat(lvls.map((lvl, i) => {
return (
<option key={i} value={lvl.db}>{`SZKOŁA ${lvl.text}`}</option>
)
}));
return (
<select defaultValue={user.level}>
{options.map(opt => opt)}
</select>
)
};
render() {
const {user} = this.props;
return (
this.returnSelect(user)
);
}
}
So what I want is to refresh the default selected value to match the value in the database. I am listening to the firebase realtime database for changes. Every time I refresh the page, the defaultValue changes, as expected, but this doesn't do it in real time. It even logs the new value, but it doesn't rerender it. What am I missing?
Ok. All I had to do was change defaultValue to value
componentWillMount() this should not be the method where you use AJAX requests
instead, user componentDidMount().
Further:
componentWillMount() will only be invoked once, before the first render() for your component, thus it will not trigger a re-render for it, you should subscribe to your firebase realtime events in componentDidMount().
Related
I am trying to rerender a component in React.
The setup:
I am using React Context to fetch some data from a Firestore database. So the fetching is Async.
My component is then fetching the data using: static contextType = MyContext and accessing via this.context
I store this context data on the components state to try to trigger a rerender whenever this data is changed.
I pass the data to a child component where it renders a list based on this data.
The problem:
I manage to update the state and even when debugging I can see my state updating to the correct data BUT the component does not rerender either the childcomponent or the list.
The expected list shows as soon as I click anything on the page so my guess is that the data is trapped in some kind of middle stage.
What I've tried:
I tried using the componentDidUpdate to make a check if the context is different than the current state and trigger a function that sets the state (I have even tried with setState function directly after the check) => Still state updates but no rerender is triggered (I can see the new data on state)
I tried using the getDerivedStateFromProps on the child component to do a check if the Props have changed and also tried storing the props in the child components own state => Still same result as before.
I am not sure what else to try, I thought that React triggers a rerender everytime state chages but probably I am doing something really wrong.
Here is my parent:
import React, { Component } from 'react';
import styles from './artistdropdown.module.css';
import { returnCollection } from '../../utils/Firebase.js';
import MyContext from '../../utils/MyContext.js';
import ArtistSelected from './ArtistSelected.js';
import ArtistsList from './ArtistsList';
export class ArtistDropdown extends Component {
static contextType = MyContext;
constructor(props) {
super(props);
this.state = {
artists: [],
currentArtist: {
id: null,
name: null
}
};
this.fetchArtist = (aId, artists) => {
const state = {
id: null,
name: null,
};
artists && artists.forEach((a) => {
if (a.id === aId) {
state = {
...state,
id: a.id,
name: a.name,
}
}
})
return state;
}
this.loadToState = (state) => {
this.setState({
...this.state,
...state,
})
}
this.click = (id) => {
this.context.handleArtistSelection(id);
this.props.handleBandDropdown();
}
}
componentDidMount() {
const aId = this.context.state.user.last_artist;
const artists = this.context.state.user.artists;
const currentArtist = this.fetchArtist(aId, artists);
const state = {
artists: artists,
currentArtist: currentArtist,
}
this.loadToState(state);
}
componentDidUpdate(props, state) {
if (this.state.artists !== this.context.state.user.artists) {
const aId = this.context.state.user.last_artist;
const artists = this.context.state.user.artists;
const currentArtist = this.fetchArtist(aId, artists);
const state = {
artists: artists,
currentArtist: currentArtist,
}
this.loadToState(state);
}
}
render() {
const bandDropdown = this.props.bandDropdown;
return (
<>
<ArtistSelected
currentBand={this.state.currentArtist.name}
handleDropdown={this.props.handleBandDropdown}
expanded={bandDropdown}
/>
<ul className={bandDropdown ? styles.band_options + ' ' + styles.expanded : styles.band_options}>
<ArtistsList artists={this.state.artists} />
</ul>
</>
)
}
}
export default ArtistDropdown
and here is my child:
import React, { Component } from 'react';
import MyContext from '../../utils/MyContext.js';
import ArtistItem from './ArtistItem.js';
export class ArtistsList extends Component {
static contextType = MyContext;
constructor(props) {
super(props);
this.state = {
artists: [],
};
this.loadToState = (state) => {
this.setState({
...this.state,
...state,
}, () => { console.log(this.state) })
}
}
componentDidMount() {
const artists = this.props.artists;
const state = {
artists: artists,
}
this.loadToState(state);
}
componentDidUpdate(props, state) {
if (state.artists !== this.state.artists) {
this.loadToState(state);
}
}
static getDerivedStateFromProps(props, state) {
if (props.artists !== state.artists) {
return {
artists: props.artists,
}
} else {
return null;
}
}
render() {
// const artistList = this.state.artists;
const artistList = this.state.artists;
const list = artistList && artistList.map((a) => {
return (<ArtistItem key={a.id} onClick={() => this.click(a.id)} name={a.name} />)
})
return (
<>
{list}
</>
)
}
}
export default ArtistsList
I'm just getting started with React. As a simple exercise, I wanted to create some components for viewing data retrieved from the JsonMonk API. The API contains 83 user records and serves them in pages of 10.
I am trying to develop a component for viewing a list of users one page at a time which I called UserList. The code for it is below:
class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {
pageNumber: 1,
users: [],
};
this.onPageNext = this.onPageNext.bind(this);
}
componentDidMount() {
this.fetchUsers(this.state.pageNumber)
.then((users) => this.setState({users: users}));
}
async fetchUsers(pageNumber) {
const response = await fetch(`https://jsonmonk.com/api/v1/users?page=${pageNumber}`);
const jsonResponse = await response.json();
return jsonResponse.data.records;
}
onPageNext() {
// ...
}
render() {
const postElements = this.state.users.map(
(props) => <User key={props._id} {...props} />);
return (
<div>
{postElements}
<div>
<button onClick={this.onPageNext}>Next</button>
</div>
</div>
);
}
}
The problem I am having pertains to the onPageNext method of my component. When the user clicks the "Next" button, I want to make a fetch for the next page of data and update the list.
My first attempt used an asynchronous arrow function passed to setState like so:
onPageNext() {
this.setState(async (state, props) => {
const nextPageNumber = state.pageNumber + 1;
const users = await this.fetchUsers(nextPageNumber);
return {pageNumber: nextPageNumber, users: users}
})
}
However, it does not seem React supports this behavior because the state is never updated.
Next, I tried to use promise .then syntax like so:
onPageNext() {
const nextPageNumber = this.state.pageNumber + 1;
this.fetchUsers(nextPageNumber)
.then((users) => this.setState({pageNumber: nextPageNumber, users: users}));
}
This works but the problem here is that I am accessing the class's state directly and not through setState's argument so I may receive an incorrect value. Say the user clicks the "Next" button three times quickly, they may not advance three pages.
I have essentially run into a chicken-or-the-egg type problem. I need to pass a callback to setState but I need to know the next page ID to fetch the data which requires calling setState. After studying the docs, I feel like the solution is moving the fetch logic out of the UsersList component, but I'm not entirely sure how to attack it.
As always, any help is appreciated.
You need to change onPageNext as below:
onPageNext() {
this.setState( prevState => {
return {pageNumber: prevState.pageNumber + 1}
}, () =>{
this.fetchUsers(this.state.pageNumber).then(users => this.setState({users: users}) )
});
}
Here is the Complete Code:
import React from "react";
export default class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {
pageNumber: 1,
users: [],
};
this.onPageNext = this.onPageNext.bind(this);
}
componentDidMount() {
this.fetchUsers(this.state.pageNumber)
.then((users) => {
console.log(users, 'users');
this.setState({users: users})
}
);
}
async fetchUsers(pageNumber) {
const response = await fetch(`https://jsonmonk.com/api/v1/users?page=${pageNumber}`);
const jsonResponse = await response.json();
return jsonResponse.data.records;
}
onPageNext() {
this.setState( prevState => {
return {pageNumber: prevState.pageNumber + 1}
}, () =>{
this.fetchUsers(this.state.pageNumber).then(users => this.setState({users: users}) )
});
}
render() {
const postElements = this.state.users.map(
(user) => <User key={user._id} {...user} />);
return (
<div>
{postElements}
<div>
<button onClick={this.onPageNext}>Next</button>
</div>
</div>
);
}
}
function User(props) {
return (
<div>
<div style={{padding: 5}}>Name: {props.first_name} {props.last_name}</div>
<div style={{padding: 5}}>Email: {props.email}</div>
<div style={{padding: 5}}>Phone: {props.mobile_no}</div>
<hr/>
</div>
);
}
Here is the Code Sandbox
I have the following TypeScript code. I simplified/remove as much as I could.
interface DataPullingPageState
{
loading: boolean;
displayedEntries: string[];
};
export class EntriesPageOne extends React.Component<{}, DataPullingPageState>
{
constructor(props: any)
{
super(props);
this.state = { loading: false, displayedEntries: [] };
}
async componentDidMount()
{
this.setState({ loading: true });
const entries = await api.loadAll();
this.setState({ loading: false, displayedEntries: entries });
}
render()
{
if (this.state.loading)
{
return <div>loading</div>;
}
else if (this.state.displayedEntries.length === 0)
{
return <div>nothing found</div>;
}
else
{
return this.state.displayedEntries.map((entry, i) => <div key={i}>{entry}</div>);
}
}
}
export class EntriesPageTwo extends React.Component<{}, DataPullingPageState>
{
constructor(props: any)
{
super(props);
this.state = { loading: false, displayedEntries: [] };
}
async componentDidMount()
{
this.setState({ loading: true });
const param = "my param";
const entries = await api.loadByStringParam(param);
this.setState({ loading: false, displayedEntries: entries });
}
render()
{
if (this.state.loading)
{
return <div>loading</div>;
}
else if (this.state.displayedEntries.length === 0)
{
return <div>nothing found</div>;
}
else
{
return this.state.displayedEntries.map((entry, i) => <div key={i}>{entry}</div>);
}
}
}
export class EntriesPageThree extends React.Component<{}, DataPullingPageState>
{
constructor(props: any)
{
super(props);
this.state = { loading: false, displayedEntries: [] };
}
async componentDidMount()
{
this.setState({ loading: true });
const param = 123;
const entries = await api.loadByNumberParam(param);
this.setState({ loading: false, displayedEntries: entries });
}
render()
{
if (this.state.loading)
{
return <div>loading</div>;
}
else if (this.state.displayedEntries.length === 0)
{
return <div>nothing found</div>;
}
else
{
return this.state.displayedEntries.map((entry, i) => <div key={i}>{entry}</div>);
}
}
}
As you can see it's three different components that all display the same but have three different ways of loading it.
I'd like to know how I can make only one component out of those three. I've already heard about HoC but don't know if they suit my case.
Yes you can HoC let's simplify your code a bit:
HoC Method
class EntriesPage extends React.Component {
// you don't need state for loading
render() {
const { loading, entries } = this.props
}
}
EntriesPage.defaultProps = { loading: true, entries: [] }
const withEntries = (apiCall) => (Page) => {
return async (props) => {
const entries = await apiCall()
<Page {...props} loading={false} entries={entries} />
}
}
Now you can compose first page like this
// PageOne
export default withEntries(api.loadAll)(EntriesPage)
// PageTwo
export default withEntries(() => api.loadByStringParam('param'))(EntriesPage)
// PageThree
export default withEntries(() => api.loadByNumberParam(123))(EntriesPage)
This will create HoC which accepts dynamic fetching method and pass the result as prop to the final component. Hope this helps
Hoc method with param as prop
You can even expose params to the final component by changing it to something like this
const withEntries = (apiCall) => (Page) => {
return async (props) => {
const { fetchParam, ...rest } = props
const entries = await apiCall(fetchParam)
<Page {...rest} loading={false} entries={entries} />
}
}
// EntriesPageComposed.tsx
export default withEntries(api.loadByStringParam)(EntriesPage)
<EntriesPageComposed fetchParams={123} />
Loader component
Or you can even make it completely isolated without HoC and pass everything as prop and make "data loader" component, which is quite common pattern in React apps, which will act only as loader for preparing next props.
const ComposedComponent = async (props) => {
const { fetchMethod, fetchParam, ...rest } = props
const entries = await fetchMethod(fetchParam)
return (
<EntriesPage {...rest} loading={false} entries={entries} />
)
}
<ComposedComponent fetchMethod={api.loadByStringParam} fetchParam={'param'} />
In this way you have initial implementation isolated and you can add new fetch methods on the fly just by passing a prop.
I have a simple React button component that when clicked should retrieve and download data on the client browser. The problem I am experiencing is that the download is triggered and the csv file downloaded before the data is passed into the href.
Here is my component:
import { Component } from 'react';
import { connect } from 'react-redux';
import { PropTypes } from 'prop-types';
import { ManageUsersSelectors } from 'selectors/Users';
import { BatchRoleActions } from 'actions/Users';
class UsersExportButton extends Component {
constructor() {
super();
this.state = {
users: ''
};
}
getUsers(){
const { userIds } = this.props;
BatchRoleActions.getAllRoleUsers(userIds)
.then((users) => {
this.setState({ users: users});
return this.state.users;
});
}
render() {
return (
<div className="roles-export-button">
<a className="button button-default" href={this.state.users} download={'roles.csv'} onClick={() => this.getUsers()} return true>Export Csv</a>
</div>
);
}
}
function mapStateToProps(state) {
const userIds = ManageUsersSelectors.batchUserIdsSelector(state);
return {
userIds: userIds
};
}
UsersExportButton.propTypes = {
text: PropTypes.string.isRequired,
data: PropTypes.array
};
export default connect(mapStateToProps)(UsersExportButton);
How can I get the getUsers()/onClick function to complete the data retrieval step before downloading?
When i debug my code I can see that the getUsers function returns data - however after the download is triggered
Make sure to bind this to your functions. In your constructor you can do:
constructor() {
super();
this.state = {
users: ''
};
this.getUsers = this.getUsers.bind(this);
}
or you can use the bind this function:
getUsers = () => {
const { userIds } = this.props;
BatchRoleActions.getAllRoleUsers(userIds)
.then((users) => {
this.setState({ users: users});
return this.state.users; // This should be removed, you can use this.state.users throughout this component.
});
}
Why not get the user data in the componentDidMount lifecycle method? It doesn't look like it needs to be called onClick.
{
// ...
componentDidMount() {
this.getUsers();
}
// ...
render() {
return (
<div className="roles-export-button">
<a className="button button-default" href={this.state.users} download={'roles.csv'}>Export Csv</a>
</div>
)
}
}
How about handling the default "link" behaviour manually to get more control? Also you should probably try to access state after setState has been executed via its callback.
e.g.
getUsers(cb){
const { userIds } = this.props;
BatchRoleActions.getAllRoleUsers(userIds)
.then((users) => {
// note the callback of setState which is invoked
// when this.state has been set
this.setState({ users: users }, cb);
});
}
const handleClick = () => {
this.getUsers(() => {
window.open(this.state.whatever)
})
}
<span onClick={handleClick}>Export Csv</span>
Listview tries to render, before the datasource is set (ComponentDidMount)
So it's always empty. if i try to call loadData() it will show it then.
How can i avoid component rendering before loading finishes?
Actions :
export const GET_COURSES = 'GET_COURSES';
export const GET_COURSES_FAILED = 'GET_COURSES_FAILED';
import getCoursesAPI from '../common/Functions';
export const getCourses = (users) => {
return dispatch => {
getCoursesAPI(users)
.then((data)=>{
const {courseList, lectureList} = data;
return dispatch(coursesSuccess(courseList, lectureList));
})
.catch(()=>{
return dispatch(coursesFailed());
});
};
}
const coursesSuccess = (courses, lectures) => {
return {
type: GET_COURSES,
payload: {
courses,
lectures
}
}
};
const coursesFailed = () => {
return {
type: GET_COURSES_FAILED
}
};
Reducer :
import * as types from "./courses.actions";
export const INITIAL_STATE = {
courses: {}
};
export default function courses(state = INITIAL_STATE, action){
const {courses, lectures} = state;
switch(action.type){
case types.GET_COURSES:
return {
...state,
courses: action.payload.courses,
lectures: action.payload.lectures
};
case types.GET_COURSES_FAILED:
return {
...state,
courses: courses ,
lectures: lectures
};
default:
return state;
}
}
Component itself :
export default class Courses extends Component {
static propTypes = {
user: PropTypes.string.isRequired,
users: PropTypes.array.isRequired,
courseDetails: PropTypes.func.isRequired,
courses: PropTypes.object.isRequired,
getCourses: PropTypes.func.isRequired,
openProfile: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
}),
dataLoaded: 0
};
}
componentWillMount(){
}
componentDidMount(){
this.props.getCourses(this.props.users);
}
componentWillReceiveProps(newProps) {
const courses = newProps.courses.courses[this.props.user]
this.setState({
dataSource: this.state.dataSource.cloneWithRows(courses),
dataLoaded: courses.length
});
}
render() {
return (
<View style={{ height: Platform.OS == "ios" ? height - 114 : height - 130 }}>
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderRow.bind(this)}
/>
</View>
);
}
}
UPDATE
#stinodes :
This fixes the issue but i think its not the proper way to do it :
componentWillMount(){
setTimeout(()=>{
console.log('!run!');
this.loadData();
}, 1000);
}
componentDidMount(){
}
async loadData(){
const {user, users, getCourses, courses} = this.props;
await getCourses(users);
const data = courses[user];
this.setState({
dataSource: this.state.dataSource.cloneWithRows(data),
dataLoaded: data.length
});
}
You can use the componentWillMount-hook instead of the componentDidMount.
Using the first, sets it before the render. The latter fires after the component is rendered for the first time.
edit: Since you're using redux to update your global state and then get your data passed into your props, you will want to fetch your courses when your component mounts. You have this in place, by loading your data in the componentDidMount-hook.
However, you won't need the async keyword, since redux will be passing the courses through your component's props anyway.
When your component's props are updated, it will do a rerender as well. If you want to have your dataSource in your state, you can use the componentWillReceiveProps-hook. This hook will be fired when your component receives new properties.
When this is called, you can add your populated datasource to your state. This won't cause a new render.
Example:
class Courses extends Component {
/* ... */
componentDidMount() {
// Will fetch your data asynchronously
this.props.getCourses(this.props.users);
}
// This hook gets the new props passed. Old props are
// still available in this.props
componentWillReceiveProps(newProps) {
const courses = newProps.courses.courses[this.props.user]
this.setState({
dataSource: this.state.dataSource.cloneWithRows(courses),
dataLoaded: courses.length
})
}
/* ... */
}