I'm trying to implement something like a Mobile Preview section where after the user do their things in the editor, the changes that they've made will be shown in the Preview section concurrently.
The issue that I'm facing now is the method that I'm using in Bulletin.js to retrieve the html content from the editor seems to be 1 step behind (as in I need to do some actions like clicking anywhere or to retrieve the last action made in the editor).
I want to make it so that the change is instant and not one step behind so that when user do things like changing font colour etc, it will be reflected to the preview section instantly.
Bulletin.js
const getContent = (htmlContentProp) => {
setHtmlContent(draftToHtml(htmlContentProp));
};
<RichTextEditor getContent={getContent} htmlContent={htmlContent} />
RichTextEditor.js
const handleEditorChange = (state) => {
setEditorState(state);
getContent(convertToRaw(editorState.getCurrentContent()));
};
Issue is here:
const handleEditorChange = (state) => {
setEditorState(state); // this is asynchronous
// so this will most likely be old value
getContent(convertToRaw(editorState.getCurrentContent()));
};
You have 2 easy options to work around this
One is to not use hook here at all, you can consume your "state" directly
const handleEditorChange = (state) => {
getContent(convertToRaw(state.getCurrentContent()));
};
Other option is to use useEffect which is more "correct" option if you for some reason need the hooks here
const handleEditorChange = (state) => {
setEditorState(state); // this is asynchronous
};
useEffect(() => {
getContent(convertToRaw(editorState.getCurrentContent()));
}, [editorState]); // this effect will trigger once the editorState actually changes value
When this line getContent(convertToRaw(editorState.getCurrentContent())) runs in the handleEditorChange function, the editorState is not yet updated with the latest value. Since React state updates are async
You can either use the state parameter in the handleEditorChange to get the latest data like below
const handleEditorChange = (state) => {
setEditorState(state);
getContent(convertToRaw(state.getCurrentContent()));
};
or use a useEffect to update state in parent based on changes in the child state.
Related
I decided to add Redux to my pet project (surprise, todolist).
Here's add entry function:
const [todoEntry, setTodoEntry] = useState('');
const addNewEntry = (e) => {
e.preventDefault();
// console.log(todoEntry);
dispatch({
type: ADD_TODO,
payload: {
prodName: todoEntry,
done: false,
favorite: false,
edit: false,
id: uuid()
}
})
setTodoEntry('');
todoEntry comes from another component like that:
<input
id='standartInput'
style={{minWidth: '250px'}}
value={todoEntry}
onChange={e => setTodoEntry(e.target.value)}
type='text'
placeholder='Add new entry (max 55 symbols)' />
Also, I use some hooks to manage my state:
const myTodoItems = useSelector((state) => state.todos[0])
const dispatch = useDispatch()
const [data, setData] = useState(myTodoItems);
And, finally, the reducer:
import { todolist } from "./todolist"
import { ADD_TODO } from '../Store/todoactions'
export const todoReducer = (state = [todolist], action) => {
switch (action.type) {
case ADD_TODO: {
const newItem = action.payload
console.log(newItem)
console.log(todolist)
return ([...todolist, newItem])
}
default:
{ return state }
}
}
The issue is:
todolist exists, I can see at browser console
newItem exists too, I also can see at browser console
BUT! When clicking on 'Add' button, state is not updated.
What I'm doing wrong?
Thanks.
const myTodoItems = useSelector((state) => state.todos[0])
You seem to be selecting only the first item, so it's not surprising that you don't see the rest.
const [data, setData] = useState(myTodoItems);
This looks like an antipattern, why do you need a state variable for something that is already tracked by Redux?
You should also use Redux Toolkit, it is not recommended to use Redux directly.
Edit:
Thank you for the codesandbox, now the problem is clear.
You are using both Redux and React state to deal with the same data, for instance you add todos through Redux but complete them with React state.
A very important principle in React/Redux is to have a single source of truth, but in your case you have two sources of truth for the todos: the Redux store and the useState hook. You use the React state for rendering and initialize it with the Redux state, but then you don't update it when a todo is added, so the UI shows outdated information.
It's fine to use sometimes Redux, sometimes useState as long as it is for independent pieces of data, but for the same data, you need to choose.
Remember that everytime you use useState(initialState) you create a new state variable/source of truth, which will become different from the initial state. Sometimes this is exactly what you want, but not here.
So I would suggest to remove the useState and go through Redux for everything you want to change about the todos (edit them, complete them, and so on).
There are some things you can have as React state (for instance the boolean "are we currently editing this specific todo item"), but then it would be much easier to have a simple useState(false) directly in the TodoItem component.
note: I am aware of the useAbortableFetch hook. Trying to recreate a simple version of it.
I am trying to create a hook that returns a function that can make an abortable fetch request.
Idea being I want this hook to hold the state and update it when needed.
The update part is controlled by another competent on input change
What I am working on currently is
function useApiData(baseUrl){
const [state, setState] = use state({
data: null,
error: null,
loading: false
})
const controller = useRef(new AbortController)
const fetchData = searchTerm => {
if(state.loading){
controller.current.abort()
controller.current = new AbortController;
}
const signal = controller.signal;
setState(state => ({...state, loading: true})
fetch(url + searchTerm, {signal})
.then(res => res.json())
.then(data => {
setState(state => ({...state, data}))
return data
})
.catch(error => {
setState(state => ({...state, error}))
})
.finally(() => setState({...state, loading: false}))
}
const fetchCallback = useCallback(debounce(fetchData, 500), [])
return {...state, search: fetchCallback}
}
Usage
function App(){
const dataState = useApiData(url);
return ComponentWithInputElement {...dataState} />
}
function ComponentWithInputElement(props){
const [value, setValue] = useState('')
const onInput = ev => {
setValue(ev.target.value)
props.search(ev.tagert.value)
}
return (
<>
<input value={value} on input={onInput}>
{props.data?.length && <render datacomp>}
</>
)
}
This seems to fail to even send the first request.
Any way to make this pattern work?
Doing this in a useEffect would be very simple but I won't have access to the input value to have it as a dep
useEffect(()=>{
const controller = new AbortController();
const signal = controller.signal
fetch(url + value, {signal})
return () => controller.abort()
},[value])
Part of what you are trying to do does not feel "right". One of the things you are trying to do is have the state of the input value (like the form state) stored in the same hook. But those are not the same bits of state, as when the user types, it is (temporarily until its saved back to the server) different to the state fetched from the server. If you reuse the same state item for both, in the process of typing in the field, you lose the state fetched from the server.
You may think, "but I don't need it any more" -- but that often turns out to be a false abstraction later when new requirements come about that require it (like you need to display some static info as well as an editable form). In that sense, in the long term it would likely be less reusable.
It's a classic case of modelling an abstraction around a single use case -- which is a common pitfall.
You could add a new state item to the core hook to manage this form state, but then you have made it so you can only ever have the form state at the same level as the fetched data -- which may work in some cases, but be "overscoping" in others.
This is basically how all state-fetch libs like react query work -- Your fetched data is separate to the form data. And the form data is just initialised from the former as its initial value. But the input is bound to that "copy".
What you want is possible if you just returned setState from an additional state item in the core hook then passed down that setState to the child to be used as a change handler. You would then pass down the actual form string value from this new state from the parent to the child and bind that to the value prop of the input.
However, I'd encourage against it, as its an architectural flaw. You want to keep your local state, and just initialise it from the fetched state. What I suggested might be OK if you intend to use it only in this case, but your answer implies reuse. I guess I would need more info about how common this pattern is in your app.
As for abort -- you just need to return the controller from the hook so the consumer can access it (assuming you want to abort in the consumers?)
I haven API endpoint, that gives me a random text, on each call. As of now, when the React Component loads for the first time, a call goes through to the API and I get the random text.
The code looks like this. I am using redux for state management.
const RandomQuoteList = ({ todo, isLoading, startLoadingTodos }) => {
useEffect(() => {
startLoadingTodos();
}, []);
const [inputValue, setInputValue] = useState(`HelloThere`);
function changeRandomText(e) {
// const item = e.target.value;
var something = Math.random();
console.log(`changeRandomText clicked + ${something}`);
setInputValue(`changeRandomText clicked + ${something}`);
console.log(inputValue);
}
const loadingMessage = <div>Loading todos...</div>;
const content = (
<GeneralWrapper>
<RandomQuoteItem todo = {todo} inputValue = {inputValue}/>
<Button onClick={changeRandomText}>Get A New Quote</Button>
</GeneralWrapper>
);
return isLoading ? loadingMessage : content;
};
const mapStateToProps = state => ({
isLoading: getTodosLoading(state),
todo: getTodos(state),
});
const mapDispatchToProps = dispatch => ({
startLoadingTodos: () => dispatch(loadTodos()),
});
export default connect(mapStateToProps, mapDispatchToProps)(RandomQuoteList);
Now, I want to be able to use a simple button click to 'refresh' the API call. That way, the API endpoint will be triggered and a fresh new text will get updated.
I have looked at the following stack over flow questions.
React: re render componet after button click
How to refresh React page/component on Button click after POST
ReactJs : How to reload a component onClick
But, I am not getting far. I am able to randomly change the state of a text component, and that is changing the text component. So, I have the random value change part taken care of.
The target component looks something like this. When I click the button on the above component, the below component updates the random text no problem.
const RandomQuoteItem = ({ todo,inputValue }) => {
//set the style for the display.
// const Container = todo.isCompleted ? TodoItemContainer : TodoItemContainerWithWarning;
const Container = TodoItemContainer;
return (
<Container>
{/* this is where you show your API response single items. */}
<h3>{todo.quoteContent}</h3>
<h4>{todo.quoteAuthor}</h4>
<h4>{todo.quoteIdentifierString}</h4>
<p>-------------------------------</p>
<h4>{todo.quoteIdentifierCompadre}</h4>
<h4>{todo.dateTimeOfResponse}</h4>
<h4>{todo.detailsAboutOperation}</h4>
<p>{inputValue}</p>
</Container>
);
}
Now, how do I link this random state change to my RandomQuoteItem state, so, it makes fresh data call?
Based on the comment from rahuuz above, I ended up with this. and it worked.
function changeRandomText(e) {
// const item = e.target.value;
var something = Math.random();
console.log(`changeRandomText clicked + ${something}`);
setInputValue(`changeRandomText clicked + ${something}`);
console.log(inputValue);
startLoadingTodos(); //this specific line solved the problem.
}
I think, it worked in my favour that I already had redux and reducers and all of that hooked up. If that was no the case, this specific solution may not have worked, I think.
Button click calls the startLoadingTodos function, which in turn calls the API and that returns data, updating the redux state, and component also updates.
This is an issue I faced, investigated, and fixed and would like to share my experience with you.
I found that when you are using useState HOOK to maintain state and then update the state using the style setState({...state, updatedProperty: updatedValue}) in an async function, you may run into some concurrency issues.
This may lead the application in some cases to lose some data due to the fact that async function keeps an isolated version of the state and may overwrite data some other component stored in the state.
The fix in short:
You need to use either a reducer to update state or use the function updater version of set state, if you are going to update the state from an async function because the function updater gets the latest updated version of state as an argument (prevState)
setState(prevState => ({...prevState, updatedProperty: updatedValue});
Long Description:
I was developing data context to manage user's contacts saved on a database which is hosted on a cloud MongoDB cluster and managed by a back end web service.
In the context provider, I used useState hook to maintain the state and was updating it like the following
const [state, setState] = useState({
contacts: [],
selectedContact: null
});
const setSelected = (contact) => setState({...state, selectedContact: contact});
const clearSelected = ()=> setState({...state, selectedContact: null};
const updateContact = async(selectedContact) => {
const res = await [some api call to update the contact in the db];
const updatedContact = res.data;
// To check the value of the state inside this function, I added the following like and found
// selectedContact wasn't null although clearSelected was called directly after this function and
// selectedContact was set to null in Chrome's React dev tools state viewer
console.log('State Value: ' + JSON.stringify(state));
//The following call will set selectedContact back to the old value.
setState({
...state,
contacts: state.contacts.map(ct=> ct._id === updatedContact._id? updatedContact : ct)
});
}
//In the edit contact form submit event, I called previous functions in the following order.
updateContact();
clearSelected();
Problem
It was found that after selecteContact is set to null then set back to the old value of the selected contact after updateContact finishes awaiting for the api call's promise.
This issue was fixed when I updated the state using the function updater
setState(prevState => ({
...prevState,
contacts: prevState.contacts.map(ct=> ct._id === updatedContact._id? updatedContact : ct)
}));
I also tried using reducer to test the behavior given this issue and found reducers are working fine even if you are going to use regular way (without the function updater) to update the sate.
[React-Redux] Issue.
I'd like to have reusable encapsulated components to be used in any app, or in any level of the app's store.
When it comes to use 'mapStatetoProps' then making the component container (injecting the state into the component as props), you always receive the whole store. This might be a pain if you want to reuse components dynamically or in other projects.
The thing is if you use the same store entry but you want to use the same component as encapsulated module they will be sharing the same data.
And also, when you are encapsulating components and you reuse them and they are deep nested in the store, you will end up needing to know where they are.
A possible ugly solution would be to implement a script going through the state inside the mapStateToProps till it finds the key matching certain name. The issue here would be to make sure the state field you want to use is unique.
I'll be more than happy to know any proper solution to this problem in an elegant way.
Or maybe we are just thinking the wrong way when talking about react-redux-sagas apps.
For the sake of the example, I'll be talking about a reusable editor component, that edits documents and submits them to server.
In the code that is using the editor, I give each editor a unique id. E.g.
const Comment = (props) => {
return <div>
<h3>Add new comment</h3>
<Editor editorId={`new-comment-${props.commentId}`} />
</div>
}
In the redux state, I have one subreducer editor with objects keyed by the editorId, so the redux state is something like:
{
otherStuff: {...},
editor: {
"new-comment-123": { isActive: true, text: "Hello there" },
"new-comment-456": { isActive: false, text: "" },
...
},
...
}
Then in Editor mapStateToProps I use selectors to get the data for the correct instance:
const mapStateToProps = (state, ownProps) => {
return {
isActive: selectors.isActive(state, ownProps.editorId),
text: selectors.text(state, ownProps.editorId)
}
}
The selectors are built in reselect style, either manually or by actually using reselect. Example:
// Manual code
export const getEditor = (state, editorId) => state.editor[editorId] || {};
export const isActive = (state, editorId) => getEditor(state, editorId).
export const text = (state, editorId) => getEditor(state, editorId).text;
// Same in reselect
import { createSelector } from 'reselect'
export const getEditor = (state, editorId) => state.editor[editorId] || {};
export const isActive = createSelector([getEditor], (editorData) => editorData.isActive);
export const text = createSelector([getEditor], (editorData) => editorData.text);
If you want to extend this to be used in multiple apps, you need to export your component, reducer and sagas. For a working example, check out https://github.com/woltapp/redux-autoloader or even http://redux-form.com
If I understand your concern correctly, you could implement mapStateToProps as if it receives the part of state you need and call it, say, mapStateToYourComponentProps, and in actual mapStateToProps you just call mapStateToYourComponentProps and pass it appropriate part of state
I found a way to make the components totally independent from state and hierarchy within the app.
Basically, each component must expose a method to set the path within the state. Then you have to initialize it when either when you import it before using it. You could also implement it in another way so you receive it inline as a prop.
It makes uses of reselect to establish the selection.
Each component knows the name of its key in the state.
The root component will import other components and it will call the setPath method of each one passing the root component path.
Then each component will call the setPath of each subcomponent passing their own location in the state. "Each parent will init their children"
So each component will set a path in the store based on the naming "parent path + local path (component key name in the store)".
This way you would be defining a nested routing with 'createSelector' method from reselect, like this: ['rootKey','subComponent1Key','subsubComponent1Key].
With this, you have the store isolation completed. Redux actions will just change the bit needed so yo have this part also covered by the framework.
It worked like a charm for me, please let me know if its good before I mark it as good.
If you have some free time, try the npm package redux-livequery (https://www.npmjs.com/package/redux-livequery) I just wrote recently.
There is another way to manage your active list.
let selector0 = (state) => state.task.isComplete;
let selector1 = (state) => state.task.taskList;
this.unsub2 = rxQueryBasedOnObjectKeys([selector0, selector1], ['isActive', 'task'], (completeTaskList) => {
// equal SQL =>
// select * from isActive LEFT JOIN taskList on isActive.child_key == taskList.child_key
console.log("got latest completeTaskList", completeTaskList);
// you can do whatever you want here
// ex: filter, reduce, map
this.setState({ completeTaskList });
}, 0);
In the reducer:
case "MARK_ACTIVE_TASK": {
let { id } = action.meta;
return update(state, { isActive: { [id]: { $set: { active: Date.now() } } } });
}
case "UNMARK_ACTIVE_TASK": {
let { id } = action.meta;
return update(state, { isActive: { $apply: function (x) { let y = Object.assign({}, x); delete y[id]; return y; } } });
}
It lets you have simpler reducer. In addition, there is no more nested selector function or filter which is really expensive operation. Putting your all logic in the same place would be great.
And it can do even more complexity operation like how to get complete and active list.
let selector0 = (state) => state.task.isComplete;
let selector1 = (state) => state.task.isActive;
let selector2 = (state) => state.task.taskList;
this.unsub3 = rxQueryInnerJoin([selector0, selector1, selector2], ['isComplete', 'isActive', 'task'], (completeAndActiveTaskList) => {
// equal SQL =>
// select * from isComplete INNER JOIN isActive on isComplete.child_key == isActive.child_key
// INNER JOIN taskList on isActive.child_key == taskList.child_key
console.log("got latest completeAndActiveTaskList", completeAndActiveTaskList);
// you can do whatever you want here
// ex: filter, reduce, map
this.setState({ completeAndActiveTaskList });
}, 0);
If you would like to get complete or active list, it's also easy to get.
The more example, please refer to the sample code => https://github.com/jeffnian88/redux-livequery-todos-example