why is props only working in render function? - javascript

I have the following code inside render and when I console log it, the correct values appear:
render() {
const { total } = this.state;
const filter = this.props.catalog.filter(word => word.markets.some(m => m === this.props.currentMarket))
console.log(filter)
console.log(this.state.filtered)
}
But when I do the same for a componentDidMount() hook it doesn't work.
export class Products extends React.Component {
constructor(props){
super(props)
this.state = {
filtered: []
};
}
componentDidMount() {
console.log(this.props.catalog.filter(word => word.markets.some(m => m === this.props.currentMarket)))
const filter = this.props.catalog.filter(word => word.markets.some(m => m === this.props.currentMarket))
this.setState({
filtered: filter
})
}
renderProductCards() {
return <ProductCard product={this.props?.catalog}/>
}
render() {
const { total } = this.state;
const filter = this.props.catalog.filter(word => word.markets.some(m => m === this.props.currentMarket))
console.log(filter)
console.log(this.state.filtered)
return (
<div>
<div className="ui divider"></div>
</div>
);
}
}
So for the first render hook I get the correct values of the filter but for componentDidMount() I get nothing as the value, I get the state value but it doesn't setState correctly and when I console log it it appears nothing.

When updating state using previous state or props, the callback argument should be used.
See https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
It should be
componentDidMount() {
this.setState((state, props)=> ({
filtered: props.catalog.filter((word)=>(
word.markets.some(m => m == props.currentMarket)
))
}));
}

I assume that you didn't call super(props) in your constructor. Try this:
constructor(props){
super(props); // Add this line to constructor
this.state = {
//states
};
}

Related

Updating a single item in React context state

So I have a Context of the following format:
class UserProvider extends Component {
constructor(props) {
super(props)
this.initialize = (details) => {
this.setState(state => {
//Setting each item individually here
//e.g state.a = details.a
})
}
this.editA = () => {
this.setState(state => {
//change A here
})
}
this.editB = () => {
this.setState(state => {
//Change B here
})
}
this.state = {
a: null,
b: null,
editA: this.editA,
editB: this.editB
}
}
render() {
return (
<User.Provider value={this.state}>
{this.props.children}
</User.Provider>
)
}
}
So for each state, I have a separate function to update it. If I want to update only a single state, what should I do?
Consider implementing a generic function so that you can control your key and the corresponding value.
i.e.
const changeField = (key, value) => this.setState({ [key]: value});
Function call
changeField('a','value_a')
changeField('b','value_b')

Props are showing empty value above `render` function while it work inside `render` function?

So I am passing props from app.js to state.jsx but while console logging the props it give me a undefined but it perfectly works inside the render function. Why that's happening?
App.js
class App extends React.Component{
state = {
data : {},
states: '',
}
async componentDidMount () {
const dataFromApi = await StateData();
this.setState({ data: dataFromApi})
}
handleStateChange = async(states) => {
const fetchedData = await StateData(states);
this.setState({data: fetchedData, states: states})
}
render(){
const {data, states} = this.state;
console.log(data);
return (
<div className="App">
<StateCard data={data} states={states}/>
</div>
);
}
}
State.jsx
export default class StateCard extends React.Component{
constructor(props) {
//this doesn't work
super(props)
const dta = this.props.data.confirmed
console.log(dta)
}
// spacing deafult value is 8px , so the 3*8=24px width column
render(){
//This works
console.log(this.props.data.confirmed)
console.log(this.props.states)
const {confirmed,active, deaths, recovered} = this.props.data
return (
<div>
<span>Confirmed : {confirmed}</span>
</div>);}
}
you can use useEffect and Function component instead of React.Component.
it's more easier then using componentdidMount and other lifecycle methods.
import React, { useEffect } from "react";
const StateCard = props => {
const { data, states } = props;
const { confirmed, active, deaths, recovered } = data;
useEffect(() => {
console.log(confirmed);
console.log(states);
}, [props]);
return (
<div>
<span>Confirmed : {confirmed}</span>
</div>
);
};
export default StateCard;

Consuming Paginated API in React Component

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

How to refactor three components, which asynchronously load and display data into one?

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.

Component not refreshing on firebase change

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().

Categories

Resources