I am trying to use chat-ui-kit-react to populate as MessageList with messages I get from another function:
Component to render Chat:
export default function Chat() {
const msgListRef = useRef();
const [history, setHistory] = useState([])
function handleHistory(response) {
if (!response?.length) {
return;
}
if (response.length > history?.length) {
msgListRef.current.scrollToBottom('smooth');
}
setHistory(response);
}
useEffect(() => {
handleHistory(getTalkHistory().talkhistory)
setInterval(() => {
handleHistory(getTalkHistory().talkhistory)
}, 5000)
}, [])
return <main>
<MessageList style={{ display: "flex" }} ref={ msgListRef }>
<Message model={{
message: 'Hello!',
position: 'single',
direction: 'incoming',
}} />
{ history?.length ? <MessageSeparator content={history[0].time.toLocaleString()} /> : null }
{ history.map(message => <Message model={{
message: converter.makeHtml(message.text),
position: 'single',
direction: message.direction,
}} /> )}
{hljs.highlightAll()}
</MessageList>
</main>
On load it will display the "Hello!" and the first time getTalkHistory returns something that is not empty (ie after it has been populated through other means), it will re-render and correctly display any messages sent/received by the time setState is called. If I increase the interval time higher, it will display more messages (if any have been sent or received). It does not matter how long the app has been running before getTalkHistory returns something for the first time, it will always work the first time, but not after.
However, afterwards it never rerenders again. No matter how many messages are being added and returned by getTalkHistory, it never updates again :/
getTalkHistory returns Chatdata as an array. If I console.log it is correctly updated at all times:
If I console.log the history.map it looks like this on the first time it renders:
However the console.log only fires two times, one time when getTalkHistory first returns something and then on the next interval (ie 5 seconds later). The second console.log can show additional messages that have been generated since, but hose messages are never rendered and the map console.log never fires again afterwards. console.log(getTalkHistory()) will fire correctly inside the setInterval and always return all messages though. So the state keeps getting updated, it just doesn't render :/
I have a global variable plyViewed in App.js that is set outside of my App component.
let plyViewed = 0;
function App() {
It monitors which move the board game is on. Below the board are some navigation buttons. If you click < I do plyViewed--, if you click I do plyViewed++. You get the picture.
This all worked fine, until I refactored!
I took the navigation buttons, who’s JSX code was all inside the App() function and put it in <MoveButtons /> in MoveButtons.js. So, I thought I could pass down plyViewed as a prop and then update the value in my code in the MoveButton child component. Then I find that props are immutable! Now I am stuck.
My code below gives an example of how I am using that plyViewed code. When someone clicks the navigation buttons, it fires an event that triggers the code to update plyViewed, although now it doesn’t work anymore because it is a prop. The rest of the game data is stored in an object called gameDetails.
I am passing down the plyViewed like this:
<MoveButtons
plyViewed={plyViewed}
// etc.
/>
A shortened version of my MoveList component is below.
plyViewed is used in multiple areas throughout my app, like gameDetails. I’ve considered putting it in the gameDetails object, but then I still have the issue of gameDetails being immutable if passed down as a prop. Then if I set plyViewed as a state variable, it becomes asynchronous and therefore unsuitable for use in calculations.
Am I thinking about this all wrong?
export default function MoveButtons(props) {
return (
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed > 0) {
props.plyViewed--;
props.board.current.setPosition(props.fenHistory[props.plyViewed]);
props.setFen(props.fenHistory[props.plyViewed]);
props.setSelectedIndex(props.plyViewed);
}
}}
>
<NavigateBeforeIcon />
</Button>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed < props.fenHistory.length - 1) {
props.plyViewed++;
props.board.current.setPosition(props.fenHistory[props.plyViewed]);
props.setFen(props.fenHistory[props.plyViewed]);
props.setSelectedIndex(props.plyViewed);
}
}}
>
<NavigateNextIcon />
</Button>
</Grid>
);
}
you are trying to update the props that are passed down from the higher-level component in your component tree, which is not possible.
You have the option to create a state using React's useState hook and passing down both the value and the dispatcher, but this is not recommended because you would be drilling props down the tree.
You can also pass the onClick events (or parts of them), up to your App component, which is an improvement to the first method but not the best practice in your case.
What you should really be doing is managing your global state using either, React's own Context API, or Redux. I think this could help you out.
While we're missing the full picture, it sounds like plyViewed should be a state and the asynchronous behaviour shouldn't prevent any computation if done properly with React.
It's easy to overlook the fact that the new state value is synchronously computed by ourselves when setting the state. We can just use that same local value to compute anything else and the async behaviour isn't affecting us at all.
onClick={() => {
if (props.plyViewed > 0) {
// New local value computed by ourselves synchronously.
const updatedPlyViewed = props.plyViewed - 1;
// Set the state with the new value to reflect changes on the app.
props.setPlyViewed(updatedPlyViewed);
// Use the up-to-date local value to compute anything else
props.board.current.setPosition(props.fenHistory[updatedPlyViewed]);
props.setFen(props.fenHistory[updatedPlyViewed]);
props.setSelectedIndex(updatedPlyViewed);
}
}}
This is a really simple pattern that should help solve the most basic issues with new state values.
Simple computations
Quick computations can be done in the render phase. The latest state values will always be available at this point. It's unnecessary to sync multiple state values if it can easily be computed from a single value, like the plyViewed here.
const [plyViewed, setPlyViewed] = useState(0);
// No special state or function needed to get the position value.
const position = fenHistory[plyViewed];
Here's an interactive example of how a simple state can be used to compute a lot of different derived information within the render phase.
// Get a hook function
const { useState } = React;
// This component only cares about displaying buttons, the actual logic
// is kept outside, in a parent component.
const MoveButtons = ({ onBack, onNext }) => (
<div>
<button type="button" onClick={onBack}>
Back
</button>
<button type="button" onClick={onNext}>
Next
</button>
</div>
);
const App = () => {
const [fenHistory, setFenHistory] = useState(["a", "b"]);
const [plyViewed, setPlyViewed] = useState(0);
const position = fenHistory[plyViewed];
const onBack = () => setPlyViewed((curr) => Math.max(curr - 1, 0));
const onNext = () =>
setPlyViewed((curr) => Math.min(curr + 1, fenHistory.length - 1));
return (
<div>
<p>Ply viewed: {plyViewed}</p>
<p>Fen position: {position}</p>
<p>Fen history: {fenHistory.join(", ")}</p>
<button
type="button"
onClick={() =>
setFenHistory((history) => [...history, `new-${history.length}`])
}
>
Add to fen history
</button>
<MoveButtons onBack={onBack} onNext={onNext} />
</div>
);
};
// Render it
ReactDOM.render(<App />, document.getElementById("app"));
button {
margin-right: 5px;
}
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
Expensive computations
If the computation takes a considerable amount of time to complete, and that doing it each render cycle is noticeably slowing down the rendering, there are some optimizations we could do.
useMemo will only recompute the memoized value when one of the
dependencies has changed. This optimization helps to avoid expensive
calculations on every render.
const [plyViewed, setPlyViewed] = useState(0);
const position = useMemo(() => /* costly computation here */, [plyViewed]);
Complex computations
If the computation has a lot of dependencies, we could use useReducer to manage a state object.
Note that the following example isn't justifying the use of useReducer and it's only used as an example of the implementation.
const initialState = {
plyViewed: 0,
fenHistory: ["a", "b"],
positionValue: "a",
};
function reducer(state, action) {
const { plyViewed, fenHistory } = state;
switch (action.type) {
case "back":
if (fenHistory.length <= 0) return state;
const newIndex = plyViewed - 1;
return {
...state,
plyViewed: newIndex,
positionValue: fenHistory[newIndex],
};
case "next":
if (fenHistory.length - 1 > plyViewed) return state;
const newIndex = plyViewed + 1;
return {
...state,
plyViewed: newIndex,
positionValue: fenHistory[newIndex],
};
case "add":
return {
...state,
fenHistory: [...fenHistory, action.value],
};
default:
throw new Error();
}
}
const App = () => {
const [{ plyViewed, fenHistory, positionValue }, dispatch] = useReducer(
reducer,
initialState
);
const onBack = () => dispatch({ type: "back" });
const onNext = () => dispatch({ type: "next" });
const onAdd = () => dispatch({ type: "add", value: 'anything' });
// ...
Async computations
If we need to get the result, for example, from a distant server, then we could use useEffect which will run once when the value changes.
const App = () => {
const [plyViewed, setPlyViewed] = useState(0);
const [position, setPosition] = useState(null);
useEffect(() => {
fetchPosition(plyViewed).then((newPosition) => setPosition(newPosition));
}, [plyViewed]);
There are a couple pitfalls with useEffect and asynchronously setting the state.
Prevent setting state on an unmounted component.
plyViewed may have changed again since the first fetch was triggered but before it actually succeeded, resulting in a race-condition
Then if I set plyViewed as a state variable, it becomes asynchronous and therefore unsuitable for use in calculations.
I think this is incorrect, you just need to start using useEffect as well:
function App() {
const [plyViewed, setPlyViewed] = useState(0);
<MoveButtons
plyViewed={plyViewed}
setPlyViewed={setPlyViewed}
/>
}
export default function MoveButtons(props) {
useEffect(() => {
props.board.current.setPosition(props.fenHistory[props.plyViewed]);
props.setFen(props.fenHistory[props.plyViewed]);
props.setSelectedIndex(props.plyViewed);
}, [props.plyViewed, props.fenHistory]);
return (
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed > 0) {
props.plyViewed--;
}
}}
>
<NavigateBeforeIcon />
</Button>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed < props.fenHistory.length - 1) {
props.plyViewed++;
}}
>
<NavigateNextIcon />
</Button>
</Grid>
);
with react-native, I want to use componentWillMount without using a class
await Font.loadAsync({
gotham_medium: require("../../assets/GothamMedium_1.ttf")
});
}
const Button = (props: TouchableOpacityProps & ButtonProps) => (
<TouchableOpacity {...props} style={styles.button}>
<Text style={styles.title}>{props.title}</Text>
</TouchableOpacity>
);
export default Button;
But I have a problem on the device :
error on the device
It says the problem is on this line (and it is):
async componentWillMount = () => {
When you use an async function, the async keyword goes right before () => (a vanilla js syntax error). Like this:
componentWillMount = async () => {
But, that's not the main problem. When not using a class, you need the useEffect hook.
So, try something like this (the whole component, and deleting componentWillMount):
const Button = (props: TouchableOpacityProps & ButtonProps) => {
useEffect(async () => {
await Font.loadAsync({
gotham_medium: require("../../assets/GothamMedium_1.ttf")
});
}, []);
return (
<TouchableOpacity {...props} style={styles.button}>
<Text style={styles.title}>{props.title}</Text>
</TouchableOpacity>
);
};
And at the top of the file:
import { useEffect } from 'react';
You can use Hooks for this,
from the docs,
If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
And
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
useEffect(async () => {
await Font.loadAsync({
gotham_medium: require("../../assets/GothamMedium_1.ttf")
});
},[]);
I am using react-apollo to fetch data through <Query /> and <Mutation />.
Thus, I want to setState when I get some data. I am getting the data in the render method.
Like this:
render() {
return (
<Query query={CAN_UPDATE_POST_QUERY} variables={{ id: this.props.router.query.postId }}>
{ payload => {
if(payload.loading) {
<div style={{width: '98%', textAlign: 'center', maxWidth: '1000px', margin: '50px auto'}}>Loading...</div>
}
if(this.isNew()){
return (
<PleaseSignIn>
{ me => (
...something
) }
</PleaseSignIn>
)
} else if (payload.data && payload.data.canUpdatePost) {
// I get payload here. Here's where I want to set new state.
this.canUpdatePost = payload.data.canUpdatePost
this.setState({ canUpdatePost: this.canUpdatePost })
return (
<PleaseSignIn>
{ me => (
...something
) }
</PleaseSignIn>
)
} else {
return (
<div style={{width: '98%', textAlign: 'center', maxWidth: '1000px', margin: '50px auto'}}>You and your mind seems to be lost. 🐡</div>
)
}
} }
</Query>
)
}
Using setState in render gives me this error:
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
How do I think in React way? And especially, how do I get my state changed when I get payload from react-apollo?
NEWBIE HERE. Sorry if silly.
Thanks in advance. :-)
In general, you should avoid using setState in your render functions. You should avoid having side affects (such as setting state) in your render function and instead should call other component functions to handle data change in your component.
The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it’s invoked, and it does not directly interact with the browser.
See the render() method reference in the react docs: https://reactjs.org/docs/react-component.html#render
You can create a function outside of your render method to fix this problem like so:
YourComponent extends React.Component {
handleCanUpdatePost = (canUpdatePos) => {
this.setState({ canUpdatePost })
}
render() {
// Your render logic here
// Whenever you want to setState do so by
// calling this.handleCanUpdatePost(payload.data.canUpdatePost)
}
}
You should also probably have a check to see if the value of state is going to change before setting it to avoid unnecessary re-renders:
handleCanUpdatePost = (canUpdatePos) => {
this.setState((state) => {
if(canUpdatePos !== state.canUpdatePost) {
return {canUpdatePost: payload.data.canUpdatePost}
}
})
}
I want to render a child component from a parent component by passing to it one object from array of objects fetched from an api.
TypeError: this.props.posts.map is not a function
renderPosts() {
return this.props.posts.map(post =>
<HomeCard key={post.id} postData={post} />
);
}
All the component:
class Home extends Component {
componentWillMount() {
this.props.getUserPosts();
}
renderPosts() {
return this.props.posts.map(post =>
<HomeCard key={post.id} postData={post} />
);
}
render() {
return (
<View>
<View style={{ paddingBottom: 55 }}>
<SearchBar />
</View>
<ScrollView>
{this.renderPosts()}
</ScrollView>
</View>
);
}
}
const mapStateToProps = state => {
const posts = state.homePost;
console.log('posts', posts);
return { posts };
};
export default connect(mapStateToProps, { getUserPosts })(Home);
I suspect this is because this.props.posts is undefined (empty or whatever default you have it set to) when Home being mounted. Since you aren't giving us any log outputs, it's hard to tell but this is a very common mistake.
The immediate fix is to give it a default value either where you define your initial state for your reducer or in mapStateToProps. The latter looking something like this (adapting your code):
const mapStateToProps = state => {
const posts = state.homePost || [];
console.log('posts', posts);
return { posts };
};
While this will fix your error, another thing you need to correct is the common misconception that whatever is in componentWillMount will execute prior to mounting. This is not true and is one of the reasons that this lifecycle method (and componentWillReceiveProps and componentWillUpdate) will be deprecated in the future.
Your code here:
componentWillMount() {
this.props.getUserPosts();
}
is asynchronous since you mention fetching this data. getUserPosts will fire but isn't guaranteed to complete before mounting. So while you think this.props.posts will be set to some value before rendering, that is not going to be the case. Hence why you are getting the not a function error message.