I am trying to achieve a particular scenario by writing an epic in redux-observable.
Elements in View
A dropdown: Dropdown has countries,
A Text box : has the user name field
A Checkbox : user selection of a persona(similar to admin vs standard)
Redux State stores the following
Country Dropdown value
Text Box value and status(disabled or enabled)
Checkbox state
Assumption
If the text box is disabled, it means the text box already has some value.
I need to call an api(accepts the country and username as input) whenever the user changes the value in the dropdown, or if he un-checks the checkbox(when value is false) or if the text box is disabled.
export default (action$: Observable<any>, state$: State$) => {
const countryValue$ =
state$.pipe(
map(o => o.countryCode),
distinctUntilChanged(),
filter(Boolean),
);
const email$ =
state$.pipe(
filter(o => o.isTextBoxLocked),
filter(o => o.isAdmin === false),
map(o => email),
distinctUntilChanged(),
);
return combineLatest(countryValue$, email$)
.pipe(map(([country, email]) =>
actions.fetchData(country, email)));
};
The problem is, if the checkbox value is true, still the api call is triggered(because it picks the last value emitted by the email stream). How do I fix this? This is happening because of my usage of combineLatest.
There are four ways to subscribe for multiple streams in rxjs.
CombineLatest, Zip, WithLatestFrom, ForkJoin
All serve its own purpose. For more details,
https://scotch.io/tutorials/rxjs-operators-for-dummies-forkjoin-zip-combinelatest-withlatestfrom
Your problem seems to be different and not based on the operator, the request is getting fired for changes in checkbox though you don't have a subscriber written to it. But its part of the state. You might have to check for filter condition and the key distinctUntilChanged() served its purpose or not.
Related
I have an event app with a signup form with a list of checkboxes that contain reasons as to why the user wants to sign up for events. On our events page we have a similar signup form, however when signing up on the events page it's already implied they want to attend events, so I want to always pass the "I want to attend events" checkbox value when they signup. Currently, I have it set to it's checked every by default but I'm confused on how to also send that value by default, storing these values in 'reasons'. When submit is clicked, nothing populates for reasons but I always need at least one reason each time
const [reasons, setReasons] = useState([])
what happens when a checkbox is checked
const onReasonChange = event => {
// Remove the reason if already selected
if (reasons.includes(event.target.value)) {
setReasons(reasons.filter(reason => reason !== event.target.value))
return
}
// Add the reason
setReasons([...reasons, event.target.value])
}
the checkbox i'm trying to get to always send the value of
<input
id="attend"
type="checkbox"
value="I want to attend events"
onChange={onReasonChange}
checked={reasons.includes("I want to attend events"), 'checked'}
/>
The simplest way to include the default value in your state and have the checkbox be checked is to initialise the state with the default value.
Your state could also work better as a Set if order isn't important
// Saves you repeating string literals
const attendReason = "I want to attend events";
const [reasons, setReasons] = useState(new Set([attendReason]));
const onReasonChange = ({ target: { checked, value } }) => {
setReasons((prev) => {
prev[checked ? "add" : "delete"](value);
return new Set(prev);
});
};
<input
name="attend"
type="checkbox"
value={attendReason}
checked={reasons.has(attendReason)}
onChange={onReasonChange}
/>
I am using the following code to render a Material UI AutoComplete field with options retrieved from an API. I would like to prevent re-rendering of the input field but the chosen country is used as a prop for the Country component which should be updated onChange. set_alpha3Code doesn't seem to update the state from within useCallback. How do I get around that?
let AC = memo(AutocompleteCountries)
function Show(props: {})
{
let [alpha3Code_, set_alpha3Code_] = useState<string>('gbr');
let onChange = useCallback((alpha3Code) => {
console.log(alpha3Code_);
set_alpha3Code_(alpha3Code);
}, []);
return (
<div>
<AC onChange={onChange}/>
{alpha3Code_ ? <Country cca3_={alpha3Code_}/> : null}
</div>
)
}
Two things jump out about that code:
onChange without value
Stale state
onChange without value
In the normal case, if you specify onChange with an input control, you have to specify value to tell the control what the current (updated) value is. The documentation for Material-UI's Autocomplete definitely suggests you need both:
Controlled states
The component has two states that can be controlled:
the "value" state with the value/onChange props combination. This state represents the value selected by the user, for instance when pressing Enter.
The same thing with a standard input prevents the input from ever changing. I haven't used the Material-UI Autocomplete, but I suspect it's similar.
Stale state
Your code is updating the value of alpha3Code_ (if onChange is called with a new value), but the console.log in that code looks at at an old one:
let onChange = useCallback((alpha3Code) => {
console.log(alpha3Code_); // <−−− Uses the *first* `alpha3Code_` only
set_alpha3Code_(alpha3Code);
}, []);
// ^^−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− Because you haven't included
// `alpha3Code_` in these dependencies
But even though that console.log shows you outdeated information, the rest of the code is fine and will work. It will re-render the AC with the updated code.
If you want to see the alpha3Code_ used for rendering, put a breakpoint on the return and look at it in the debugger (or move the console.log outside the onChange handler to above the return).
I recently posted a question with regards to sending and receiving data via a websocket. In the interest of not duplicating most of the work, the original question is found here: Socket.io emitting undefined variable React
I have since modified my app slightly so that each id received from the server gets added or removed to a disabled array (if the focus parameter received from the server is true, add the id and if not, remove it). This is so that I can disabled/enable questions based off the focus sent.
The slight adjustments I've made are three fold, all within the App.js file
1. Add disabled array to state
const [disabled, setDisabled] = useState([])
2. Include disabled parameter to the question
<TextArea
cols={50}
helperText="Optional helper text here; if message is more than one line text should wrap (~100 character count maximum)"
id="text2"
invalidText="Invalid error message."
labelText="Text area label"
placeholder="Placeholder text"
rows={4}
onFocus={() => setFocusTrue('text2')}
onBlur={() => setFocusFalse('text2')}
disabled={disabled.indexOf('text2')!==-1}
/>
You will note in the above that the disabled parameter will be set to true if the questionId is included in the disabled array (therefore disabling the question) and false if not
3. Add questionId to disabled state array
useEffect(() => {
socket.on("message", ({ user, id, focus }) => {
console.log(user, "clicked on", id, "with focus", focus)
console.log('adding', id, 'to disabled')
setDisabled(prevDisabled => [...prevDisabled, id]);
console.log("disabled:", disabled)
})
}, [])
For now, I am just adding all the questionIds coming from the server to the disabled array so I can test whether they are indeed getting added. But this is my issue in that the disabled array is always empty (see screenshot below). The id variable exists as can be seen in the console log output that precedes setting the new disabled state, but it is not getting added as expected.
EDIT:
As per the comments, I can see that the disabled array is in fact getting updated, if I console.log(disabled) just before rendering. However, if I apply any sort of logic to the array, I get an error. The below is the modified useEffect which contains some logic (essentially, to add or remove an id to the array)
useEffect(() => {
socket.on("message", ({ user, id, focus }) => {
console.log(user, "clicked on", id, "with focus", focus)
console.log('adding', id, 'to disabled')
if (focus) {
console.log('not my user and focus is true')
setDisabled(prevDisabled => [...prevDisabled, id])
console.log("disabled after adding", disabled)
} else {
let filteredArray = disabled.filter(idToRemove => idToRemove !== id)
setDisabled({disabled: filteredArray});
}
console.log("disabled:", disabled)
})
}, [])
When I click on a textbox, the id of the question gets added to the disabled array as expected, but when I click out of it, I get the following error:
disabled.indexOf is not a function
This referring to disabled={disabled.indexOf('text1')!==-1}
I think your code is fine, if you are trying to console.log the most resent state right after you setting it, it won't work why? setState is asynchronous it might not work real time as you expected.
what you actually want to try is add a useEffect and listen for changes of the disabled state
useEffect(() => {
console.log("disabled", disabled);
}, [disabled]); // since disabled is in the dependency array this hook function will call, every time when the disabled gets updated.
// so you are so sure the disabled is getting updated correctly
or just do a simple console.log(disabled) right before the render and see.
and your modified version of useEffect is incorrect as I see, it should be corrected as
....
const filteredArray = disabled.filter(idToRemove => idToRemove !== id)
setDisabled(filteredArray);
....
Background
I have added a toggle button in my Redux app that allows someone to toggle whether they like a specific tv show or not. When I click the button once, I can toggle it on (make the button active), but when I click it again the value does not revert back to its original value (the button is not active).
What I've Already Tried
The semantic-ui-react documentation gives an example of usuage, but I am not sure how to incorporate this logic into my current code since I am already using a handleWatchlist callback for another change to state.
I know the problem lies in the way I am handling the value being passed to the active property of my button. Here, I am passing watchlistValue which is always either true or false.
<Popup
trigger={
<Button
toggle
active={watchlistValue}
onClick={(_) => this.handleWatchlist(programId,
watchlistValue)}
icon='heart'
/>}
content="Add to Watchlist."
size='tiny'/>
Here is my current handleWatchlist method.
handleWatchlist = (programId, watchlistValue) => {
this.props.toggleWatchlist(programId, watchlistValue)
}
Here is how I have defined the program whose watchlist value (the heart button) I wish to toggle.
let program = this.props.program ? this.props.program : this.props.programs[this.props.match.params.id - 1]
let programId = this.props.program ? this.props.program.id : null
let watchlistValue = this.props.program ? this.props.program.watchlist : null
Here is a link to the whole file if you need to see the all on one page.
The toggle function already updates the value of my watchlist item in the database. In the DOM, clicking it makes it go active once. Unfortunately, it will not toggle off (to a false value).
Thank you in advance for your time and please let me know if I need to provide additional details.
In your reducer change
let programToBeToggled = copyOfPrograms.find(program => program.id === action.id);
programToBeToggled.watchlist = !action.watchlist;
to
let programIndex = copyOfPrograms.findIndex(program => program.id === action.id);
copyOfPrograms[programIndex].watchlist = !copyOfPrograms[programIndex].watchlist;
copyOfPrograms.find is creating a new object which you are toggling the watchList value of. However that does not change the boolean in copyOfPrograms which you are then returning from the reducer.
Full case with console logs to help spot bug:
case 'TOGGLE_WATCHLIST':
/*
Make a deep copy of our current state by using JSON.stringify to turn our array of programs into a string.
After we have created the stringifiedPrograms, we then use JSON.parse to turn it back into a brand new array of objects.
We then take our copyOfPrograms and find the specific program that we want to update (here we find it by id).
After isolating that program, we update the value of watchlist.
Then we return a copy of state, with the program key set to our copyOfPrograms array of objects.
Updating my programToBeToggled watchlist value still updates it in the copyOfPrograms array.
*/
console.log('state.programs:');
console.log(state.programs);
let stringifiedPrograms = JSON.stringify(state.programs);
console.log('stringifiedPrograms:');
console.log(stringifiedPrograms);
let copyOfPrograms = JSON.parse(stringifiedPrograms);
console.log('copyOfPrograms:');
console.log(copyOfPrograms);
let programIndex = copyOfPrograms.findIndex(program => program.id === action.id);
copyOfPrograms[programIndex].watchlist = !copyOfPrograms[programIndex].watchlist;
console.log('copyOfPrograms after switcheroo:');
console.log(copyOfPrograms);
return {...state, programs: copyOfPrograms};
Your action is missing the watchlist key, leading the toggle value to always be true.
From actions/toggleWatchlist.js:
// dispatched action has a `type` and an `id`
.then(res => dispatch({type: 'TOGGLE_WATCHLIST', id: programId}))
from reducers/programReducer.js:
// action.watchlist is undefined so !action.watchlist is always true
programToBeToggled.watchlist = !action.watchlist
Be careful with when and where you're toggling the values as well, you should only toggle them once, either in the action or the reducer so make sure you don't fix the above issue only to toggle in both action and reducer, negating the toggle.
I have a button that submits selected values to api. Once this has been submitted I am then trying to turn button state to disable and rest the values selected back to original state before nay where selected.
This is what I am doing on upload handle:
handleStatusEditsUpload = () => {
const { value, status } = this.state;
this.setState({
value: selected,
status: {}
});
};
In my real version locally status is clearing, status is used when changing all values at the same time by clicking the header title, a dialog appears to change all values in that column.
The main one I am having trouble is with the value. Value is populated with a new array that looks at table cell and row.
Here is demo to my project: https://codesandbox.io/s/50pl0jy3xk
Why isnt the state changing? any help appreciated as always.
What is happening is that you are mutating state in your "handleValue" method.
const newValue = [...this.state.value]; // this holds reference
newValue[rowIdx][cellIdx] = val; // so that here your state is mutated ( and const "selected" with it)
In the long term you probably should change your data structure a bit, so it would be easier to merge updates in to your state value. But a quick fix would be to clone the state value before mutating it:
handleValue = (event, val, rowIdx, cellIdx) => {
const newValue = _.cloneDeep(this.state.value); // no reference anymore
newValue[rowIdx][cellIdx] = val; // update the cloned value
this.setState({
value: newValue
});
};
I just ran your code in the sandbox you provided and it's throwing errors when you click the confirm button (trying to spread non-iterable). Once that is corrected, the state updates correctly. See my fork below:
https://codesandbox.io/s/385y99575m
I've left in a few console logs so you can see the component state updating when your onClick fires.
Why are you passing in your props to the handleStatusEditsUpload method? It doesn't take an argument. Was this just part of your debugging process?