So, let talk about lifecycle of a component React.
I have my model file is name is Firebase.js inside I have all my methods can trigger my data.
Firebase.js
export const getUserShare = () => {
let arr = [];
db.ref(`/music/${userID()}`).on("value", (snapshot, key) => {
snapshot.forEach(items => {
let element = items.val();
arr.push(element);
});
});
return arr;
};
And I have a Profil.js component and inside I import the method that I want (For this example _getUserData())
On my component I set a state arrSharing, at the name of my method _getUserData() the output of this is an array with all my data.
Profil.js :
this.state = {
arrSharing: getUserShare()
};
The Profil.js is only accessible with my router or if you put the link on your browser.
Now I rendering the data in my render function like that :
{this.state.arrSharing.map((items, i) => {
return (
<div key={i} className="share-music">
<h5 className="share-music__title">
{items.artiste} - {items.title}
</h5>
<p className="share-music__platform">
par {items.displayName} sur #{items.radio}
</p>
</div>
);
})}
Now running the application and if I go for the first time on the Profil.js component the data was not loaded, BUT if I go on another component and after I return on Profil.js the data was loaded.
The problem come from the component is not rendering for the first time. When I use componentDidMount() I have the same problem. And it's very problematic for my case.
I see there is a problem with asynchronous action handling.
Change your getUserShare method to accept callback:
export const getUserShare = (callback) => {
db.ref(`/music/${userID()}`).on("value", (snapshot, key) => {
const arrSharing = [];
snapshot.forEach(items => {
let element = items.val();
arrSharing.push(element);
});
callback(arrSharing);
});
};
Then do this in Profile.js:
this.state = {
arrSharing: [],
}
componentDidMount() {
getUserShare((arrSharing) => {
this.setState({ arrSharing })
})
}
Related
A have two files, with two functional components A and B, in the first component A, i have a specialFunction that gets called with onClick, what i want to do is raise an event in specialFunction when it's called, and then in component B add a Listener for the event in specialFunction.
Component A:
function specialFunction(){
//raise the event and send some data
}
Component B:
//contains a listener that does some work when specialFunction is called, example:
(data) => {console.log("am called:",data)};
1. Create notifier class using observer pattern
class ChangeNotifier {
subscribers = [];
subscribe(callback) {
this.subscribers.push(callback);
}
unsubscribe(callback) {
const index = this.subscribers.indexOf(callback);
if (index > -1) {
this.subscribers.splice(index, 1);
}
}
notifyAll(data) {
this.subscribers.forEach(callback => callback(data));
}
}
2. ComponentA receives notifier as a prop and used to notify all subscribers
const ComponentA = ({ notifier }) => {
const triggerNotifier = () => {
notifier.notifyAll('Some data that will subscribers receive');
}
return <div>{/** Some content */}</div>
}
3. ComponentB receives notifier and subscribes to it to receive data sent by from ComponentB
const ComponentB = ({ notifier }) => {
useEffect(() => {
const callbackFn = data => {/** Do whatever you want with received data */ }
notifier.subscribe(callbackFn);
return () => notifier.unsubscribe(callbackFn);
}, [])
}
4. App holds both component. Create instance of notifier there and pass as a props
const App = () => {
const dataNotifier = new ChangeNotifier();
return <div>
<ComponentA notifier={dataNotifier} />
<ComponentB notifier={dataNotifier} />
</div>
}
If you have components on different levels deeply nested and it is hard to pass notifier as a prop, please read about React Context which is very helpful when you want to avoid property drilling
React Context
Here's implementation with context
class ChangeNotifier {
subscribers = [];
subscribe(callback) {
this.subscribers.push(callback);
return this.unsubscribe.bind(this, callback);
}
unsubscribe(callback) {
const index = this.subscribers.indexOf(callback);
if (index > -1) {
this.subscribers.splice(index, 1);
}
}
notifyAll(data) {
this.subscribers.forEach(callback => callback(data));
}
}
const NotifierContext = React.createContext();
const ComponentA = () => {
const { notifier } = useContext(NotifierContext);
const triggerNotifier = () => {
notifier.notifyAll('Some data that will subscribers receive');
}
return <div><button onClick={triggerNotifier}>Notify</button></div>
}
const ComponentB = () => {
const { notifier } = useContext(NotifierContext);
useEffect(() => {
const callbackFn = data => { console.log(data) }
notifier.subscribe(callbackFn);
return () => notifier.unsubscribe(callbackFn);
}, [notifier])
}
Now all components wrapped in NotifierContext.Provider (no matter how deep they are nested inside other components) will be able to use useContext hook to receive context value passed as value prop to NotifierContext.Provider
const App = () => {
const dataNotifier = useMemo(() => new ChangeNotifier(), []);
return <NotifierContext.Provider value={{ notifier: dataNotifier }}>
<ComponentA />
<ComponentB />
</NotifierContext.Provider>
}
export default App;
Last but not least, I guess you can avoid context or properties drilling and just create instance of ChangeNotifier in some utility file and export it to use globally...
Andrius posted a really good answer, but my problem was that the two components, one of them is used as an API, and the other had a parent component, am a beginner so maybe there is a way to use them but i just didn't know how.
The solution that i used, (maybe not the best) but did the job was to dispatch a custom event in a Promise from the specialFunction:
function specialFunction(){
new Promise((resolve) => {
console.log("am the promise");
document.dispatchEvent(event);
resolve();
});
And add a Listener in the other component using a useEffect hook:
useEffect(() => {
let handlePreview = null;
new Promise((resolve) => {
document.addEventListener(
"previewImg",
(handlePreview = (event) => {
event.stopImmediatePropagation();
//Stuff...
})
);
return () =>
window.removeEventListener("previewImg", handlePreview, false);
});
}, []);
Thank you for your help.
I am trying to curb useEffect related to nested components. Here are the components:
Parent (it receives data from API):
const ListOfLots = (props) => {
const initial = {listLots: props.lots, form: props.form}
const [lots, setLots] = useState(initial);
useEffect(() => {
setLots({
listLots: props.lots,
form: props.form
});
});
return (
<div>
{
lots.listLots.map(function(lot) {
return <Lot key={lot.uuid} lot={lot} procForm={lots.form}/>
})
}
</div>
)
}
Nested:
const Lot = (props) => {
const initial = {currLot: props.lot, form: props.form};
const [lot, setLot] = useState(initial);
let getWinningBid = (offers) => {
for (let i = 0; i < offers.length; i++) {
console.log("BOOM!!!");
if (offers[i].isTrue === true) {
return offers[i].pip;
}
}
}
return (
...
)
}
While I am using no dependencies at parent's useEffect, I have got an infinite invoking of console.log("BOOM!!!"), that is, of course, unacceptable, but my Nested component rerendered. When I try to use the following type of dependencies at useEffect: [], [lots.form], [lots.listLots] or [lots.listLots.length] my Nested component is not rerendered: it stays blank. So the result is the following: I have an infinite useEffect loop or not-working(?) useEffect.
Is there any way in this case to handle the useEffect?
Use
useEffect(() => {
setLots({
listLots: props.lots,
form: props.form
});
}, [props.lots, props.form]);
This triggers the callback only if the value of props.lots, props.form is changed else it won't be triggered on every rerender as in case of no second argument.
A similar question here might help you find better explanations.
I have my state and I want to display the component if the value is true but in the console I receive the error message Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state my code
import React, { useState} from "react";
import { useToasts } from "react-toast-notifications";
const Index = () => {
const [test, setTest]= useState(true);
const { addToast } = useToasts();
function RenderToast() {
return (
<div>
{ addToast('message') }
</div>
)}
return (
<div>
{test && <RenderToast /> }
</div>
)
}
You cannot set state during a render. And I'm guessing that addToast internally sets some state.
And looking at the docs for that library, you don't explicitly render the toasts. You just call addToast and then the <ToastProvider/> farther up in the tree shows them.
So to make this simple example works where a toast is shown on mount, you should use an effect to add the toast after the first render, and make sure your component is wrapped by <ToastProvider>
const Index = () => {
const { addToast } = useToasts();
useEffect(() => {
addToast('message')
}, [])
return <>Some Content here</>
}
// Example app that includes the toast provider
const MyApp = () => {
<ToastProvider>
<Index />
</ToastProvider>
}
how i can display the toast based on a variable for exemple display toast after receive error on backend?
You simply call addToast where you are handling your server communication.
For example:
const Index = () => {
const { addToast } = useToasts();
useEffect(() => {
fetchDataFromApi()
.then(data => ...)
.catch(error => addToast(`error: ${error}`))
}, [])
//...
}
I want to create an app with comments feature. I am trying with the code like this:
response.data.forEach((el, idx, arr) => {
const newMessage = <CommentMessage username={el.username} message={el.message}/>
ReactDOM.render(newMessage, this.commentListRef.current)
})
I am using MySQL. Axios for HTTP Requests. And Next.js for the framework.
Full code:
import React from 'react'
import ReactDOM from 'react-dom'
import styles from './comments-list.module.css'
class CommentMessage extends React.Component {
constructor(props) {
super(props)
}
render() {
return (<div>
<b>{this.props.username}</b>
<span>: </span>
<span>{this.props.message}</span>
</div>)
}
}
class CommentsList extends React.Component {
constructor(props) {
super(props)
this.commentListRef = React.createRef()
const comments = []
}
loadComments() {
const axios = require('axios')
axios.get('/api/getcomments')
.then(response => {
response.data.forEach((el, idx, arr) => {
const newMessage = <CommentMessage username={el.username} message={el.message}/>
ReactDOM.render(newMessage, this.commentListRef.current)
})
})
.catch(err => {
console.log(err)
})
}
render() {
return (<div ref={this.commentListRef} onLoad={this.loadComments()}>
</div>)
}
}
export default CommentsList
But it only render this:
Expected this:
You're going about this pretty strangely; I don't know if that's on purpose or not. Regardless, the recommended approach would be to store the comments as part of your component's state, and update the state when you get the comments.
Like this:
class CommentsList extends React.Component {
constructor(props) {
super(props)
this.state = {
comments: []
};
this.commentListRef = React.createRef()
const comments = []
}
loadComments() {
const axios = require('axios')
axios.get('/api/getcomments')
.then(response => {
this.setState({
comments: response.data
});
})
.catch(err => {
console.log(err)
})
}
componentDidMount(){
this.loadComments();
}
render() {
return (<div ref={this.commentListRef}>
(this.state.comments.map(comment => (
<CommentMessage username={comment.username} message={comment.message}/>
)))
</div>)
}
}
Also, your onLoad wasn't working as you had expected. It will call loadComments every time the component renders, and I don't even know if onLoad is a proper event on a div.
At any rate, if you absolutely wanted to do it the way you did it, you would have to mount each node into its own container. As you have it right now, each comment is overwriting the contents of commentListRef. So you'd have to create a new element, append that to commentListRef, and mount the react component to that:
loadComments() {
const axios = require('axios')
axios.get('/api/getcomments')
.then(response => {
response.data.forEach((el, idx, arr) => {
const element = document.createElement('div');
this.commentListRef.current.appendChild(element);
const newMessage = <CommentMessage username={el.username} message={el.message}/>
ReactDOM.render(newMessage, element)
})
})
.catch(err => {
console.log(err)
})
}
ReactDOM.render will only render one component for a given container. From the docs:
Any existing DOM elements inside are replaced when first called. Later calls use React’s DOM diffing algorithm for efficient updates.
Basically when you call ReactDOM.render in a loop, React is treating each given component as an update to the previous component, rather than rendering each individually.
Best practice is to render a single component at the root container (usually called <App>). However it seems you've already done this as these ReactDOM.render calls are happening within another component. Generally, you should only need to use ReactDOM.render once within an app.
Instead you can store the data in the CommentsList component's state and just return the children components from the parent's render method.
For example:
class CommentsList extends React.Component {
constructor(props) {
super(props)
this.state = {
comments: [],
}
}
loadComments = () => {
const axios = require('axios')
axios.get('/api/getcomments')
.then(response => {
this.setState(prev => ({...prev, comments: response.data}));
})
.catch(err => {
console.log(err)
})
}
render() {
const { comments } = this.state;
return (
<React.Fragment>
{comments.map(e => (
<CommentMessage key={e.id} username={e.username} message={e.message}/>
))}
</React.Fragment>
)
}
}
Note: I've also passed a key to the CommentMessage component to give each child a stable identity (see docs for more info). Had to guess, but I assume a comment would have an id value, if not, you can choose a different unique value for the comment to use as a key.
Also I'd recommend moving to React Hooks over class-based components—a lot easier to work with once you get a grasp on hooks.
I'm a beginner in React and Redux. I've been working on this project where I finally figured out how to store an array as a state. Now, the only problem I'm having is, trying to figure out how to pass that state to another file.
Here are the two files
-Hue.js
-ColorShop.js
In Hue.js, I made an API and saved the contents into an array of objects called productJSON
Hue.js
class Hue extends React.Component {
constructor() {
super();
this.state = {
productJSON: []
};
}
componentWillMount() {
fetch('numberendpoint.json')
.then(results => {
return results.json();
}).then(data => {
let colorArray = [] //initialize array to receive json data
for (let i =0; i < data.length; i++) {
colorArray.push(data[i])
}
let productJSON = JSON.stringify(productArray)
this.setState({productJSON: productJSON});
})
}
render() {
return (
<div>
<div className="container2">
{this.state.productJSON}
</div>
</div>
)
}
}
Now, I'm trying to pass productJSON to another file in the same folder, ColorShop.js. I need to replace _colors (which was reading from a static json file) with productJSON.
ColorShop.js
import Hue from './Hue.js'
const TIMEOUT = 100
Hue productJSON={this.state.productJSON} <---- my attempt to get it to work
export default { // I need to replace '_colors' with productJSON
getColors: (cb, timeout) => setTimeout(() => cb(_colors), timeout || TIMEOUT),
}
I don't want to make another class in ColorShop.js, I just want to import this.state.productJSON into it, is that possible? Any pointers are greatly appreciated!!
Update: used the solution suggested by Rahamin. Now I have this code below, all contained within the the "Hue" class. But I'm still getting errors.
import React from 'react'
const TIMEOUT = 100
let productJSON;
class Hue extends React.Component {
constructor() {
super();
this.state = {
products: [],
};
this.getColors = this.getColors.bind(this)
}
componentDidMount() {
fetch('http://tech.work.co/shopping-cart/products.json')
.then(results => {
return results.json();
}).then(data => {
let colorArray = []
for (let i =0; i < data.length; i++) {
colorArray.push(data[i])
}
console.log("jsonproduct=" + JSON.stringify(productArray))
productJSON = JSON.stringify(colorArray)
this.setState({productJSON: productJSON});
});
}
render() {
return (
<div>
<div className="container2">
{this.state.productJSON}
</div>
</div>
)
}
}
export default {
getColors: (cb, timeout) => setTimeout(() => cb(({ productJSON: value})), timeout || TIMEOUT), // here is where I am getting an error -- "value" is undefined. I'm not sure I was meant to put "value" there or something else...very new to React so its conventions are still foreign to me.
}
What do you want to do? If the 'other file' is a helper function, you can just pass it a parameter, as you do in any function in any programming language:
From Hue you call colorShop(productsJson) and get back a result that you can render in Hue (colorShop starting with a lowercase character, otherwise React will think it is a component). It seems that the 'other file' can just be a function in the Hue.js file...
ColorShop can also be a component that gets productsJson as a prop and renders it after modification. ColorShop doesn't need to be a class, it can be a functional component. But this doesn't seem to be required by your example.
Here is the code (not complete - see comments) after inserting colorShop as a function into the class component. And you can pass it a value and get a returned value, or have it set the state, whatever you like:
import React from 'react';
const TIMEOUT = 100;
class Hue extends React.Component {
constructor() {
super();
this.state = {
productJSON: []
};
this.getColors = this.getColors.bind(this);
}
componentWillMount() {
fetch('numberendpoint.json')
.then(results => {
return results.json();
}).then(data => {
let colorArray = []; //initialize array to receive json data
for (let i = 0; i < data.length; i++) {
colorArray.push(data[i]);
}
let productJSON = JSON.stringify(productArray);
this.setState({ productJSON: productJSON });
});
}
render() {
// call getColor from wherver you need to call it.
// for example:
// getColors();
return (
<div>
<div className="container2">
{this.state.productJSON}
</div>
</div>
);
}
getColors(cb, timeout) {
setTimeout(() => cb(colors), timeout || TIMEOUT);
// Here you can setState({ productJSON: value });
// Or you can return a value to the caller
}