CKEditor5 React component's autosave plugin holds old state value (useState) - javascript

Setup : React + CKEditor5 Online build with AutoSave plugin
I have a simple Notes application. App Component gets a list of note IDs from DB and uses Note component to display them. Note component gets an ID in the props. I am reusing the Note component to create a new note. In which case, it will receive NEW as ID
<div>
{notes.map((note: any, index) => (
<div key={index} className="row g-5 gx-xxl-8">
<div className="col-xxl-8">
<Note id={note}></Note>
</div>
</div>
))}
</div>;
Note component uses useState hook to store note details. An initial empty value is set for the state.
// Initial setup as a new Note
const newNote: NoteModel = {
id: "EMPTY",
content: "",
};
const [currentNote, setCurrentNote] = useState(newNote);
React useEffect hook is used to make a call to DB and update the state. New note is saved to DB on the first change.
useEffect(() => {
if (props.id !== "NEW") {
setTimeout(() => {
// Simulate DB call. Set note after 1 second
const noteDetails = {
id: props.id,
content: "Hello. This is the new content",
};
setCurrentNote(noteDetails);
}, 1000);
}
}, [props.id]);
I am using CKEditor5's autosave plugin. Notice the data prop referencing the state variable. That updates the editor to the new value every time state is updated.
<CKEditor
editor={CustomEditor}
data={currentNote.content}
config={{
autosave: {
waitingTime: 2000, // in ms
save(editor: any) {
return saveNote(editor);
},
},
removePlugins: ["MediaEmbedToolbar"],
}}
onReady={(editor: Editor) => {
}}
onChange={(event: EventInfo, editor: Editor) => {
console.log("inside change :: " + JSON.stringify(currentNote)); // Prints the correct state values
}}
onBlur={(event: EventInfo, editor: Editor) => {
}}
onFocus={(event: EventInfo, editor: Editor) => {
}}
/>
Here is the saveNote function used in the autosave configuration. I am making a decision between create and update API using the current state.
const saveNote = (editor: any) => {
console.log("inside save :: " + JSON.stringify(currentNote)); // Always prints {"id":"EMPTY","content":""}
if (currentNote.id === "EMPTY") {
return new Promise<void>((resolve) => {
// Make a CreateNote API call which returns the new ID
// And save the new ID back to the component state
resolve();
});
} else {
return new Promise<void>((resolve) => {
// Make an UpdateNote API call
resolve();
});
}
};
Problem::
saveNote function always has 'EMPTY' in the currentNote state variable. Even after I updated the state in the useEffect hook. So, it always calls the create API.
Why doesn't saveNote get the latest state? Any ideas on better ways to do it are welcome too!

Related

Passing Async State to Next.js Component via Prop

I'm fetching WordPress posts asynchronously via getStaticProps()...
export async function getStaticProps({ params, preview = false, previewData }) {
const data = await getPostsByCategory(params.slug, preview, previewData)
return {
props: {
preview,
posts: data?.posts
},
}
}
... and passing them to useState:
const [filteredArticles, setFilteredArticles] = useState(posts?.edges)
Then, I pass the state to a component:
router.isFallback ? (
// If we're still fetching data...
<div>Loading…</div>
) : (
<ArticleGrid myArticles={filteredArticles} />
This is necessary because another component will setFilteredArticles with a filter function.
But when we are passing the state to ArticlesGrid, the data is not ready when the component loads. This is confusing to me since we passing the state within a router.isFallback condition.
Even if we set state within useEffect...
const [filteredArticles, setFilteredArticles] = useState()
useEffect(() => {
setFilteredArticles(posts)
}, [posts?.edges])
... the data arrives too late for the component.
I'm new to Next.js. I can probably hack my way through this, but I assume there's an elegant solution.
Let's look at some useEffect examples:
useEffect(() => {
console.log("Hello there");
});
This useEffect is executed after the first render and on each subsequent rerender.
useEffect(() => {
console.log("Hello there once");
}, []);
This useEffect is executed only once, after the first render.
useEffect(() => {
console.log("Hello there when id changes");
}, [props.id]);
This useEffect is executed after the first render, every time props.id changes.
Some possible solutions to your problem:
No articles case
You probably want to treat this case in your ArticleGrid component anyway, in order to prevent any potential errors.
In ArticleGrid.js:
const {myArticles} = props;
if(!myArticles) {
return (<div>Your custom no data component... </div>);
}
// normal logic
return ...
Alternatively, you could also treat this case in the parent component:
{
router.isFallback ? (
// If we're still fetching data...
<div>Loading…</div>
) : (
<>
{
filteredArticles
? <ArticleGrid myArticles={filteredArticles} />
: <div>Your custom no data component... </div>
}
</>
)
}
Use initial props
Send the initial props in case the filteres haven't been set:
const myArticles = filteredArticles || posts?.edges;
and then:
<ArticleGrid myArticles={myArticles} />

Only rerender the affected child in list of components while state resides in parent React

I'm building a chat app, I have 3 components from parent to child in this hierarchical order: Chat, ChatLine, EditMessage.
I'm looping through messages state in Chat to display multiple ChatLine components as a list, and I pass some state to ChatLine and then to EditMessage.
I need the state :
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
to remain in the parent component Chat so I can have access to it later there.
Anyway, now when I click on the Edit button, the EditMessage component shows a textarea, and I'm setting state onChange in it, but everytime I click the Edit button or type a letter in the textarea all the components rerender as I see in React DevTool Profiler, even the children that didn't get affected, I only need the Chat and affected ChatLine to rerender at most.
The whole code is available in CodeSandbox, and deployed in Netlify.
And here it is here also :
(Chat.js)
import { useEffect, useState } from "react";
import ChatLine from "./ChatLine";
const Chat = () => {
const [messages, setMessages] = useState([]);
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
useEffect(() => {
setMessages([
{ id: 1, message: "Hello" },
{ id: 2, message: "Hi" },
{ id: 3, message: "Bye" },
{ id: 4, message: "Wait" },
{ id: 5, message: "No" },
{ id: 6, message: "Ok" },
]);
}, []);
return (
<div>
<p>MESSAGES :</p>
{messages.map((line) => (
<ChatLine
key={line.id}
line={line}
editValue={editValue}
setEditValue={setEditValue}
editingId={editingId}
setEditingId={setEditingId}
/>
))}
</div>
);
};
export default Chat;
(ChatLine.js)
import EditMessage from "./EditMessage";
import { memo } from "react";
const ChatLine = ({
line,
editValue,
setEditValue,
editingId,
setEditingId,
}) => {
return (
<div>
{editingId !== line.id ? (
<>
<span>{line.id}: </span>
<span>{line.message}</span>
<button
onClick={() => {
setEditingId(line.id);
setEditValue(line.message);
}}
>
EDIT
</button>
</>
) : (
<EditMessage
editValue={editValue}
setEditValue={setEditValue}
setEditingId={setEditingId}
editingId={editingId}
/>
)}
</div>
);
};
export default memo(ChatLine);
(EditMessage.js)
import { memo } from "react";
const EditMessage = ({ editValue, setEditValue, editingId, setEditingId }) => {
return (
<div>
<textarea
onKeyPress={(e) => {
if (e.key === "Enter") {
// prevent textarea default behaviour (line break on Enter)
e.preventDefault();
// updating message in DB
updateMessage(editValue, setEditValue, editingId, setEditingId);
}
}}
onChange={(e) => setEditValue(e.target.value)}
value={editValue}
autoFocus
/>
<button
onClick={() => {
setEditingId(null);
setEditValue("");
}}
>
CANCEL
</button>
</div>
);
};
export default memo(EditMessage);
const updateMessage = (editValue, setEditValue, editingId, setEditingId) => {
const message = editValue;
const id = editingId;
// resetting state as soon as we press Enter
setEditValue("");
setEditingId(null);
// ajax call to update message in DB using `message` & `id` variables
console.log("updating..");
};
The problem is that all of the child components see their props change any time any of them is in the process of being edited, because you're passing the current editing information to all of the children. Instead, only pass the current editing text (editValue) to the component being edited, not to all the others.
ChatLine doesn't use editValue when it's not the instance being edited. So I'd do one of two things:
Use a different component for display (ChatLine) vs. edit (ChatLineEdit). Almost the entire body of ChatLine is different depending on whether that line is being edited or not anyway. Then only pass editValue to ChatLineEdit.
Pass "" (or similar) as editValue to the one not being edited. In the map in Chat: editValue={line.id === editingId ? editValue : ""}.
Pass an "are equal" function into memo for ChatLine that doesn't care what the value of editValue is if line.id !== editingId. By default, memo does a shallow check of all props, but you can take control of that process by providing a function as the second argument. For instance:
export default memo(ChatLine, (prevProps, nextProps) => {
// "Equal" for rendering purposes?
return (
// Same chat line
prevProps.line === nextProps.line &&
// Same edit value setter (you can leave this out, setters from `useState` never change)
prevProps.setEditValue === prevProps.setEditValue && // ***
// Same editingId
prevProps.editingId === prevProps.editingId &&
// Same editingId setter (you can leave this out too)
prevProps.setEditingId === prevProps.setEditingId && // ***
(
// Same edit value...
prevProps.editValue === prevProps.editValue ||
// OR, we don't care because we're not being edited
nextProps.line.id !== nextProps.editingId
)
);
});
This is fragile, because it's easy to get the check wrong, but it's another option.
I would go for #1. Not even passing props to components that they don't need is (IMHO) the cleanest approach.

How can manipulate redux data after dispatch it?

I have a search screen, contain Input And TopTabs "Songs, Artists",
When I get data from API after a search I make two things
1- I setState to appear the TopTab Component "true/false"
2- dispatch an action to save Songs & Artists Data in redux store.
that works fine.
But in topTab component, as I say before I have tow tabs "songs, artists"
For example, In the Songs component, I want to manipulate the data to achieve my case so in componentDidMount I Map the songs array from redux and push the new data into the component state.
But it's not working fine!
At the first time, I got songs from redux as empty [] although it's saved successfully in redux store when I get data from API
So how can I handle this case to not mutate the data?
Search.js "Main screen"
onSearch = async () => {
const {searchText} = this.state;
if (searchText.length > 0) {
this.setState({onBoarding: false}); // to appear the TopTab Component
try {
let response = await API.post('/search', {
name: searchText,
});
let {
data: {data},
} = response;
let artists = data.artists.data;
let songs = data.traks.data;
this.props.getResult(songs, artists);
}
catch (err) {
console.log(err);
}
}
render(){
<View style={styles.searchHeader}>
<Input
onChangeText={text => this.search(text)}
value={this.state.searchText}
onSubmitEditing={this.onSearch}
returnKeyType="search"
/>
</View>
{this.state.onBoarding ? (
<SearchBoard />
) : (
<SearchTabNavigator /> // TopTabs component
)}
}
SongsTab
...
componentDidMount() {
console.log('props.songs', this.props.songs); // Empty []
let All_tunes = [];
if (this.props.songs?.length > 0) {
console.log('mapping...');
this.props.songs.map(track =>
All_tunes.push({
id: track.id,
name: track.name,
url: URL + track.sounds,
img: URL + track.avatar,
}),
);
this.setState({All_tunes});
}
}
...
const mapStateToProps = state => {
return {
songs: state.searchResult.songs,
};
};
Edit
I fix the issue by using componentDidUpdate() life cycle
If you have any other ways tell me, please!
SongsTab
manipulateSongs = arr => {
let All_tunes = [];
arr.map(track =>
All_tunes.push({
id: track.id,
name: track.name,
url: URL + track.sounds,
img: URL + track.avatar,
}),
);
this.setState({All_tunes});
};
componentDidMount() {
if (this.props.songs?.length > 0) {
this.manipulateSongs(this.props.songs);
console.log('mapping...');
}
}
componentDidUpdate(prevProps) {
if (prevProps.songs !== this.props.songs) {
this.manipulateSongs(this.props.songs);
}
}
The problem you're referring to has to do with the way asynchronous code is handled in JavaScript (and in turn react-redux). When your component initially mounts, your redux store passes its initial state to your SongsTab.js component. That seems to be an empty array.
Any API call is an asynchronous action, and won't update the redux store until the promise has resolved/rejected and data has been successfully fetched. Any HTTP request takes much longer to complete than painting elements to the DOM. So your component loads with default data before being updated with the response from your API call a number of milliseconds later.
The way you've handled it with class-based components is fine. There are probably some optimizations you could add, but it should work as expected. You might even choose to render a Spinner component while you're fetching data from the API as well.
If you want a different approach using more modern React patterns, you can try and use the equivalent version with React hooks.
const Songs = ({ fetchSongs, songs, ...props }) => {
React.useEffect(() => {
// dispatch any redux actions upon mounting
// handle any component did update logic here as well
}, [songs])
// ...the rest of your component
}
Here are the docs for the useEffect hook.

Redux-React : How to pass the updated state to function onChange?

I am new to React and was trying a simple thing. I do not understand how to modify the state and pass it to the function. Please find my code below :
I am skipping the redundant code, only passing the point of focus, everything works fine except this functionality.
state = {
card: this.props.card // No probelm here , the props are correctly received in my component
};
I am trying update the state onChange and use this state value in my dispatcher to generate a new state after this event. Please find the code snippet of this functionality here :
<select
class="form-control m-1"
value={this.state.card.type}
onChange={e => {
let newType = e.target.value;
this.setState(prevState => ({
card: {
...prevState.card,
type: newType
}
}));
console.log(this.state.card) // **This gives me the old state, not updated one**
this.props.updateCard(this.state.card) // Correctly receiving the updateCard Props ,
}}
>
<option value="ABC">Option1</option>
<option value="DEF">Option2</option>
</select>
My Dispatcher :
updateCard: card=> {
dispatch({ type: "UPDATE_CARD", card: card})}
My Reducer :
case "UPDATE_CARD": {
console.log("INSIDE REDUCER");
console.log(action.card);
return {
cards: state.cards.map(card=>
card.id === action.card.id ? action.card: card
)
};
}
Please help on this. I did search a lot of stuff here but nothing was helpful.
That's because setState is not synchronous:
...
onChange ={e => {
let newType = e.target.value;
this.setState(prevState => ({
card: {
...prevState.card,
type: newType
}
}), () => {
console.log(this.state.card) // will give you the new value
// you should also do any updates to redux state here to trigger
// re-renders in the correct sequence and prevent race conditions
});
console.log(this.state.card) // **This gives me the old state, not updated one**
this.props.updateCard(this.state.card) // Correctly receiving the updateCard Props ,
}}
...

React Event Handling: Marking as Favorite item on Instant in

So in my recipe App, users are able to mark or unmark recipes as their favorite.
The only thing I can't wrap my head around is How to make it instant. my current code supports makes a post call to mark the recipe as favorite but you see the change of icon (i.e the filled one) they have to refresh the page.
I do need some suggestion on how can I make it work on the click.
Here is my code:
class CuisineViewById extends Component {
constructor(props) {
super(props);
this.state = {
user: {},
access_token: '',
};
this.toggleFavorite = this.toggleFavorite.bind(this);
}
componentDidMount() {
this.props.getUser(() => {
this.props.getAccessToken(this.props.user.profile.sub, () => {
console.log(this.props.user);
this.props.getCuisineById(this.props.match.params.id, this.props.accessToken);
this.props.getFavoriteRecipes(this.props.accessToken);
});
});
}
toggleFavorite(userID, recipeID, marked) {
const userpreference = {
userid: userID,
recipe: recipeID,
favorite: marked
};
axios
.post('/api/userpreference', userpreference, {
headers: {'access_token': this.props.access_token}
})
.then(res => console.log(res));
}
displayFavorite = recipeId => {
let favoriteRecipes = this.props.userPreferred;
for (var i = 0; i < favoriteRecipes.length; i++) {
if (favoriteRecipes[i].recipe === recipeId) {
return true;
} else {
}
}
};
render() {
const that = this;
const {user} = this.props;
const {cuisine} = this.props;
return (
<CuisineTileHeading
label={cuisine.label}
totalNoRecipes={cuisine.totalRecipes]}
key={cuisine.id}
>
{cuisine.recipes && cuisine.recipes.map(function(asset, index)
{
let marked = recipe.isFavorite ? 'no' : 'yes';
return (
<RecipesCards
title={recipe.title}
description={recipe.description}
chef={recipe.owner}
lastUpdated={recipe.lastUpdated}
recipeType={recipe.assetType}
key={'RecipesCard' + index}
thumbnail={recipe.thumbnailBase64}
recipeId={recipe.id}
cuisine={cuisine}
favorite={that.displayFavorite(recipe.id)}
toggleFavorite={() =>
that.toggleFavorite(userId, recipe.id, marked)
}
/>
);
})}
</CuisneTileHeading>
)
}
}
const mapStateToProps = state = ({
cuisine : state.cuisine.cuisne,
user: state.user.user,
userPreferred: state.recipe.userPrefered,
accessToken: state.asset.accessToken
)}
In my component did mount, I am calling functions to get user information, then access token and then cuisines and then user favorite recipes.
toggleFavorite is the function that makes a recipe favorite or not favorite.
displayFavorite is a function that return either true or false is recipe id matches to the recipe ID store in userpreference object.
Right now, you compute that "this recipe is favorite" from a function that returns true or false.
ReactJS has no way to automatically trigger a re-rendering of the favorite icon since it is not linked to the recipe's state at all.
If you put "isFavorite" in the recipe's state and toggle that to true or false with the onClick event, which will change the recipe's state value for "isFavorite", React should know to call a re-render on the recipe card's icon ... then you just make sure it outputs the HTML for a filled icon when true and empty icon when false. React will know to re-render all DOM elements linked to that "slice" of the state, "isFavorite recipe" in this case.
TL;DR: leverage React's state concept instead of computing if the recipe is favorited by the user through a function which does not modify the state, since re-renders are done by React when the state changes.

Categories

Resources