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 ,
}}
...
Related
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!
I have a problem were the react state did not extract the data in the react-bootstrap select form-control tag. Below are what I did so far:
state structure
this.state = {
... more list
list: [] // in this list, each item contains { id: 1, name: 'some name' }
}
Some class
componentDidMount() {
// ...api fetch and here
axios.get('https://api-server.com/test/on/something').then(response => {
this.setState(state => {
response.data['something'].map(g => {
return state.list.push(g);
})
}, () => {
console.log(this.state.list) // it did set the state
})
});
}
render()
<Form.Control as="select">
{
// the problem is here! can't extract the data
this.state.list.map(g => {
return <option value={g.id}>{g.name}</option>
})
}
</Form.Control>
I am not sure why it didn't display each data but I am certainly sure that it did set each item in the state correctly.
You're mutating the existing state state.list.push(g); which should never be done in React. it's not a good way.
Try something like this instead, cloning everything:
this.setState(prevState => ({ ...prevState, list: [ ...prevState.list, ...response.data['something'] ] }))
I just tried your API URL and it didn't return anything.
Can you check your console to see if it returns something?
I would like to use the awesome react-widgets DropDownList to load records on demand from the server.
My data load all seems to be working. But when the data prop changes, the DropDownList component is not displaying items, I get a message
The filter returned no results
Even though I see the data is populated in my component in the useEffect hook logging the data.length below.
I think this may be due to the "filter" prop doing some kind of client side filtering, but enabling this is how I get an input control to enter the search term and it does fire "onSearch"
Also, if I use my own component for display with props valueComponent or listComponent it bombs I believe when the list is initially empty.
What am I doing wrong? Can I use react-widgets DropDownList to load data on demand in this manner?
//const ItemComponent = ({item}) => <span>{item.id}: {item.name}</span>;
const DropDownUi = ({data, searching, fetchData}) => {
const onSearch = (search) => {
fetchData(search);
}
// I can see the data coming back here!
useEffect(() => {
console.log(data.length);
}, [data]);
<DropDownList
data={data}
filter
valueField={id}
textField={name}
onSearch={onSearch}
busy={searching} />
};
Got it! This issue is with the filter prop that you are passing to the component. The filter cannot take a true as value otherwise that would lead to abrupt behavior like the one you are experiencing.
This usage shall fix your problem:
<DropdownList
data={state.data}
filter={() => true} // This was the miss/fix 😅
valueField={"id"}
textField={"name"}
busy={state.searching}
searchTerm={state.searchTerm}
onSearch={(searchTerm) => setState({ searchTerm })}
busySpinner={<span className="fas fa-sync fa-spin" />}
delay={2000}
/>
Working demo
The entire code that I had tried at codesandbox:
Warning: You might have to handle the clearing of the values when the input is empty.
I thought that the logic for this was irrelevant to the problem statement. If you want, I can update that as well.
Also, I added a fakeAPI when searchTerm changes that resolves a mocked data in 2 seconds(fake timeout to see loading state).
import * as React from "react";
import "./styles.css";
import { DropdownList } from "react-widgets";
import "react-widgets/dist/css/react-widgets.css";
// Coutesy: https://usehooks.com/useDebounce
import useDebounce from "./useDebounce";
interface IData {
id: string;
name: string;
}
const fakeAPI = () =>
new Promise<IData[]>((resolve) => {
window.setTimeout(() => {
resolve([
{
name: "NA",
id: "user210757"
},
{
name: "Yash",
id: "id-1"
}
]);
}, 2000);
});
export default function App() {
const [state, ss] = React.useState<{
searching: boolean;
data: IData[];
searchTerm: string;
}>({
data: [],
searching: false,
searchTerm: ""
});
const debounceSearchTerm = useDebounce(state.searchTerm, 1200);
const setState = (obj: Record<string, any>) =>
ss((prevState) => ({ ...prevState, ...obj }));
const getData = () => {
console.log("getting data...");
setState({ searching: true });
fakeAPI().then((response) => {
console.log("response: ", response);
setState({ searching: false, data: response });
});
};
React.useEffect(() => {
if (debounceSearchTerm) {
getData();
}
}, [debounceSearchTerm]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<DropdownList
data={state.data}
filter={() => true} // This was the miss/fix 😅
valueField={"id"}
textField={"name"}
busy={state.searching}
searchTerm={state.searchTerm}
onSearch={(searchTerm) => setState({ searchTerm })}
busySpinner={<span className="fas fa-sync fa-spin" />}
delay={2000}
/>
</div>
);
}
Let me know if you have more queries on this 😇
So it i think that list should be loaded a then you can filtering your loaded data.In your example on the beginning you don't have value so list is empty, you tape in some text and then value of list re render but it look like is not filtered.....
However I look through code base, and it's look like is not ready until you don't set manually open prop drop down list component. In getDerivedStateFromprops, next data list is read only if in next props is open set. to true
From DropDwonList
static getDerivedStateFromProps(nextProps, prevState) {
let {
open,
value,
data,
messages,
searchTerm,
filter,
minLength,
caseSensitive,
} = nextProps
const { focusedItem } = prevState
const accessors = getAccessors(nextProps)
const valueChanged = value !== prevState.lastValue
let initialIdx = valueChanged && accessors.indexOf(data, value)
//-->> --- -- --- -- -- -- -- - - - - - - - - - --- - - --------
//-->>
if (open)
data = Filter.filter(data, {
filter,
searchTerm,
minLength,
caseSensitive,
textField: accessors.text,
})
const list = reduceToListState(data, prevState.list, { nextProps })
const selectedItem = data[initialIdx]
const nextFocusedItem = ~data.indexOf(focusedItem) ? focusedItem : data[0]
return {
data,
list,
accessors,
lastValue: value,
messages: getMessages(messages),
selectedItem: valueChanged
? list.nextEnabled(selectedItem)
: prevState.selectedItem,
focusedItem:
(valueChanged || focusedItem === undefined)
? list.nextEnabled(selectedItem !== undefined ? selectedItem : nextFocusedItem)
: nextFocusedItem,
}
}
I would try:
<DropDownList
data={data}
filter
open
valueField={id}
textField={name}
onSearch={onSearch}
busy={searching} />
};
if it will be works, then you just have to
manage your open state by yourself.
Edit: I don't understand the reason for downvotes, this was a good question and no other questions on this site solved my issue. I simply preloaded the data to solve my issue but that still doesn't solve the problem without using functional components.
I'm trying to pass users last message into the ListItem subtitle prop but I can't seem to find a way to return the value from the promise/then call. It's returning a promise instead of the value which gives me a "failed prop type". I thought about using a state but then I don't think I could call the function inside the ListItem component anymore.
getMsg = id => {
const m = fireStoreDB
.getUserLastMessage(fireStoreDB.getUID, id)
.then(msg => {
return msg;
});
return m;
};
renderItem = ({ item }) => (
<ListItem
onPress={() => {
this.props.navigation.navigate('Chat', {
userTo: item.id,
UserToUsername: item.username
});
}}
title={item.username}
subtitle={this.getMsg(item.id)} // failed prop type
bottomDivider
chevron
/>
);
You could only do it that way if ListItem expected to see a promise for its subtitle property, which I'm guessing it doesn't. ;-) (Guessing because I haven't played with React Native yet. React, but not React Native.)
Instead, the component will need to have two states:
The subtitle isn't loaded yet
The subtitle is loaded
...and render each of those states. If you don't want the component to have state, then you need to handle the async query in the parent component and only render this component when you have the information it needs.
If the 'last message' is something specific to only the ListItem component and not something you have on hand already, you might want to let the list item make the network request on its own. I would move the function inside ListItem. You'll need to set up some state to hold this value and possibly do some conditional rendering. Then you'll need to call this function when the component is mounted. I'm assuming you're using functional components, so useEffect() should help you out here:
//put this is a library of custom hooks you may want to use
// this in other places
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
const ListItem = ({
title,
bottomDivider,
chevron,
onPress,
id, //hae to pass id to ListItem
}) => {
const [lastMessage, setLastMessage] = useState(null);
const isMounted = useIsMounted();
React.useEffect(() => {
async function get() {
const m = await fireStoreDB.getUserLastMessage(
fireStoreDB.getUID,
id
);
//before setting state check if component is still mounted
if (isMounted.current) {
setLastMessage(m);
}
}
get();
}, [id, isMounted]);
return lastMessage ? <Text>DO SOMETHING</Text> : null;
};
I fixed the issue by using that promise method inside another promise method that I had on componentDidMount and added user's last message as an extra field for all users. That way I have all users info in one state to populate the ListItem.
componentDidMount() {
fireStoreDB
.getAllUsersExceptCurrent()
.then(users =>
Promise.all(
users.map(({ id, username }) =>
fireStoreDB
.getUserLastMessage(fireStoreDB.getUID, id)
.then(message => ({ id, username, message }))
)
)
)
.then(usersInfo => {
this.setState({ usersInfo });
});
}
renderItem = ({ item }) => (
<ListItem
onPress={() => {
this.props.navigation.navigate('Chat', {
userTo: item.id,
UserToUsername: item.username
});
}}
title={item.username}
subtitle={item.message}
bottomDivider
chevron
/>
);
I am attempting to use react-widgets with onSearch with Multiselect. I can see that onSearch gets called with searchTerm. But, the response never becomes part of the options/data for Multiselect.
I've tried returning the results in an Array or in an object with the key data to the Array.
const getOptions = searchTerm => {
return [{ _id: 1, username: 'Bob' }];
}
const TagItem = ({ username }) => <span>{username}</span>
const MultiSelectUsers = ({ input, ...rest }) =>
<Multiselect {...input}
onBlur={() => input.onBlur()}
value={input.value || []}
valueField="_id"
tagComponent={TagItem}
itemComponent={TagItem}
onSearch={getOptions}
{...rest}
/>
Bonus: How do I use onSearch with Promise?
A couple of things need to happen here.
1) onSearch merely calls a callback, ie, it updates nothing. The update needs to be done via data. (Eg, use state in MultiSelectUsers)
2) The trick is that #1 won't work, unless you set filter={false} in Multiselect