How to persist counter variables using useReducer in React - javascript

I have two objects that I'm trying to loop through using an increment and decrement function. I have the variables being stored in localStorage and the expectation is that no matter which value has been selected last, upon refresh or reload, the counter should still work. However, I've experienced some unexpected occurrences upon using the increment and decrement functions where the variable does not line with the index of the object as shown by the reducer state.
const increment = (props) => {
return (props.state + 1) % props.length;
};
const decrement = (props) => {
return (props.state - 1 + props.length) % props.length;
};
const colors = [
{ id: 0, type: "Red" },
{ id: 1, type: "Blue" },
{ id: 2, type: "Green" },
{ id: 3, type: "Yellow" }
];
For example, at times when I call the state, it will tell me that the color is Yellow and the index is 2, and is generally inconsistent. I've tried storing the counter variable within localStorage and calling from that in hopes that it will synchronize the counter with the intended variable, however, this has not worked.
Here is a demo with CodeSandbox. I'm still relatively new to React and I'm not sure if using a counter is the best method for this problem.
Here is a reproducible example of the output I've received (upon clearing localStorage and refreshing the app) and the expected output for the shapes.
Output:
Square (next) Square (previous) Circle (previous) Square (previous) Octagon (next) Triangle
Expected output:
Square (next) Circle (previous) Square (previous) Octagon (previous) Triangle (next) Octagon

Having forked and refactored your original sandbox here there were a few challenges.
Firstly, the reducer actions were expanded to include increment and decrement. The example provided in React Hooks Reference has an example of incrementing and decrementing, though reducers are not just applicable for counting. The dispatch of your reducer will set the state, so having Action.Set is redundant.
Let's take a look at one of the original buttons' onClick methods. Originally, colorCount was being decremented and then an update to color occurs based on the state at the time of click, not when the state is updated. To visualize this on the original demo, log the state before and after setData.
onClick={() => {
setData({
payload: decrement({
state: state.colorCount,
length: colors.length
}),
name: "colorCount"
});
setData({
payload: colors[state.colorCount].type,
name: "color"
});
}}
Now, the same onClick calls the decrement method.
onClick={() => {
decrement({
name: "colorCount"
});
}}
The decrement method, moved to the context, just calls the dispatch with proper type and payload containing the name of the value to update.
const decrement = (payload) => {
dispatch({
type: ACTIONS.DECREMENT,
payload
});
};
Lastly, the reducer updates the states colorCount paired with its prefix color and shapeCount paired with its prefix shape
const reducer = (state, action) => {
// Verify which value we need to update along with its count
const isColor = action.payload.name.includes("color");
// Select the proper option in context
const options = isColor ? colors : shapes;
switch (action.type) {
case ACTIONS.INCREMENT:
// Increment the count for use in the value and type setting
const incrementedCount =
(state[action.payload.name] + 1) % options.length;
return {
...state,
// Set the new count
[action.payload.name]: incrementedCount,
// Set the new color or shape type
[isColor ? "color" : "shape"]: options[incrementedCount].type
};
case ACTIONS.DECREMENT:
// Decrement the count for use in the value and type setting
const decrementedCount =
(state[action.payload.name] - 1 + options.length) % options.length;
return {
...state,
// Set the new count
[action.payload.name]: decrementedCount,
// Set the new color or shape type
[isColor ? "color" : "shape"]: options[decrementedCount].type
};
default:
return state;
}
};
As far as updating the localStorage on update of a value, the easiest way is another useEffect dependent on the state values. Feel free to update the localStorage how and when you want, but for the purposes of keeping the state on reload the simplest approach was kept.
useEffect(() => {
localStorage.setItem("colorCount", JSON.stringify(state.colorCount));
localStorage.setItem("color", JSON.stringify(state.color));
localStorage.setItem("shapeCount", JSON.stringify(state.shapeCount));
localStorage.setItem("shape", JSON.stringify(state.shape));
}, [state.colorCount, state.color, state.shape, state.shapeCount]);
To the point made about contexts, this counter example does benefit from simplicity. The reducer can be used all within the App. Contexts are best used when passing down props to children becomes cumbersome.

What you've provided here is not sufficient for a minimum reproducible example. We can't offer much help if your problem is only happening "at times" -- please provide specific cases of what steps you take to obtain a specific problem.
Generally speaking, I think it might simplify your code to use two separate state variables. Context seems like overkill for this use case.
const [colorIdx, setColorIdx] = useState(0);
const [shapeIdx, setShapeIdx] = useState(0);
And, as a style note, it is usually a good idea to avoid inline function definitions. The following is much more readable, for example:
const incrementColorIdx = () => {
setColorIdx((colorIdx + 1) % colors.length);
}
...
<button onClick={incrementColorIdx}>Next</button>

Related

React-table with select and dropdown

Edit Note:Link with working version at bottom of post
I have run into some really weird behavior with my project
I have the parent which gets some data from a database and passes it to the child (which is a popup). The child then uses useEffect to detect when the data is ready (black part) and sets the value in the child (blue part).
useEffect(() => {
console.log(datachannels.length)
if (datachannels.length > 1) {
let newValueState = []
let c = 0
datachannels.forEach(element => {
newValueState.push({ id: c, dc: element.dataChannel, axis: "y" })
c += 1
});
setValueState(newValueState)
console.log(newValueState)
console.log("trigger")
}
}, [datachannels])
Then when a user changes a option this code runs to update it(Green part)
const handler = (event, id) => {
console.log(valueState)
const value = event.value
console.log(valueState.length)
if (valueState.length > 0) {
let newValueState = valueState
newValueState.forEach(key => {
if (key.id == id) {
console.log(key)
key.axis = value
}
});
setValueState(newValueState)
}
}
With inspect i can see that the value is not empty but the handler function trying to use it is telling me its empty. When giving valueState a default value and after useEffect code runs, I try and access it but it just returns the default value. I have no idea what is causeing this or what to google so if anyone can shead some light on this, I would be very happy
Update 1: Codesandebox (new link below) I have not been able to get the bug to show yet
Update 2:Code in sandbox now has error. As you can see when useEffect runs the object has 3 values but when the user changes the dropdown it only has one value
Update 3: To see the bug in the sandbox change one of the axis drop downs
Update 4: Porok12 answer works in the sand box but is not working in my actully project and i have no idea what is going on
Update 5: So when using Porok12 code and feeding in datachannels the same way as the sandbox it does work but with some other weird behavior that im hopping can point to the problem. When changing a dropdown this is outputted to the console. From input was changed to (3) [{},{},{}] is the correct behavior but then everything rerenders as seen from the other messages. I have no idea why it is behaving differently from porok12 code as it is the same. But this does point out a problem with how datachannels is being passed in? In the parent object I am storing the channels like this
if (data !== undefined) {
let parseData = JSON.parse(data);
let toPush = []
Object.keys(parseData).forEach(key => {
toPush.push({ "dataChannel": key })
});
setDataChannels(toPush);
And feeding it to the child like this <BuilderTable handleupdate={handleUpdate} datachannels={dataChannels} show={modalShow} onHide={() => setModalShow(false)} />
Update 6: So its now working with Porok12 code after i changed the useMemo [] to use valueState instead of datachannel. The only problem now is that the dropdown reset to the starting value every time you change it
Update 7: With the change from update 6 and feeding valueStatus into the dropdowns the problem is not fixed.
Update 8: https://codesandbox.io/s/spring-water-2ucq8u?file=/src/GraphBuilderFull.js Here is the sandbox link with a working version
Side note: you can change
let c = 0
datachannels.forEach(element => {
newValueState.push({ id: c, dc: element.dataChannel, axis: "y" })
c += 1
});
to
const newValueState = datachannels.map((element, index) => ({id: index, dc: element.dataChannel, axis: "y"}))
The problem is that you use useMemo that way:
const selectChannelTableColumn = React.useMemo(
() => [
{
Header: "Axis Selection",
Cell: ({ row }) => (
<div>
<Dropdown
id={row.id}
options={axis}
onChange={(e) => handler(e, row.id)}
placeholder="y"
/>
</div>
)
}
],
[] // missing `datachannels` xor `valueState` dependency
);
If you will add datachannels to dependencies it will work. You should also move handler definition to this useMemo code.
Here is fixed sandbox example
The changes I made:
Move handler definition to selectChannelTableColumn code
Fix handler logic
Move selectChannelTableColumn under (meaning below not inside) your useEffect
Add datachannels to useMemo hook dependencies

Why is useState not triggering re-render?

I've initialized a state that is an array, and when I update it my component does not re-render. Here is a minimal proof-of-concept:
function App() {
const [numbers, setNumbers] = React.useState([0, 1, 2, 3]);
console.log("rendering...");
return (
<div className="App">
{numbers.map(number => (
<p>{number}</p>
))}
<input
type="text"
value={numbers[0].toString()}
onChange={newText => {
let old = numbers;
old[0] = 1;
setNumbers(old);
}}
/>
</div>
);
}
Based on this code, it seems that the input should contain the number 0 to start, and any time it is changed, the state should change too. After entering "02" in the input, the App component does not re-render. However, if I add a setTimeout in the onChange function which executes after 5 seconds, it shows that numbers has indeed been updated.
Any thoughts on why the component doesn't update?
Here is a CodeSandbox with the proof of concept.
You're calling setNumbers and passing it the array it already has. You've changed one of its values but it's still the same array, and I suspect React doesn't see any reason to re-render because state hasn't changed; the new array is the old array.
One easy way to avoid this is by spreading the array into a new array:
setNumbers([...old])
You need to copy numbers like so let old = [...numbers];
useState doesn't update the value only if it has changed so if it was 44 and it became 7 it will update. but how can it know if an array or object have changed. it's by reference so when you do let old = numbers you are just passing a reference and not creating a new one
Others have already given the technical solution. To anyone confused as to why this happens, is because setSomething() only re renders the component if and only if the previous and current state is different. Since arrays in javascript are reference types, if you edit an item of an array in js, it still doesn't change the reference to the original array. In js's eyes, these two arrays are the same, even though the original content inside those arrays are different. That's why setSomething() fails do detect the changes made to the old array.
Note that if you use class components and update the state using setState() then the component will always update regardless of whether the state has changed or not. So, you can change your functional component to a class component as a solution. Or follow the answers provided by others.
You can change state like this
const [state, setState] = ({})
setState({...state})
or if your state is Array you can change like this
const [state, setState] = ([])
setState([...state])
I was working on an array that had objects in it and I tried few of the above.
My useState is :
const [options, setOptions] = useState([
{ sno: "1", text: "" },
{ sno: "2", text: "" },
{ sno: "3", text: "" },
{ sno: "4", text: "" },
]);
Now I want to add more options with blank field on a click of a button I will use the following way to achieve my purpose:
<button
onClick={() => {
setOptions([...options, { sno: newArray.length + 1, text: "" }]);
}}
>
This solved my problem and I was able to re render the component and added an object to the array.
introduces an array of the component that is not the one of the hook. for instance:
const [numbers, setNumbers] = useState([0, 1, 2, 3]);
var numbersModify = []; //the value you want
and at the end:
setNumbers(numbersModify)
modify this numbersModify, when the hook refreshes it will return to 0 numbersModify and the hook will keep the state. Therefore, the problem of not seeing the changes will be eliminated.
:D
//define state using useState hook
const [numbers, setNumbers] = React.useState([0, 1, 2, 3]);
//copy existing numbers in temp
let tempNumbers = [...numbers];
// modify/add no
tempNumbers.push(4);
tempNumbers[0] = 10;
// set modified numbers
setNumbers(tempNumbers);
I dont have any proud of this but it works
anotherList = something
setSomething([])
setTimeout(()=>{ setSomething(anotherList)},0)
useState is a React hook which provides functionality of having State in a functional component.
Usually it informs React to re-render the component whenever there is change in useState variables.
{
let old = numbers;
old[0] = 1;
setNumbers(old);
}
In the above code since you are referring to the same variable it stores the reference not the value hence React doesn't know about the latest changes as the reference is same as previous.
To overcome use the below hack, which will not copy the reference instead it's a deep copy(copies the values)
{
let old = JSON.parse(JSON.stringify(numbers));
old[0] = 1;
setNumbers(old);
}
Happy coding :)

Dynamically created custom form components in react

See this gist for the complete picture.
Basically I will have this form:
When you click the plus, another row should appear with a drop down for day and a time field.
I can create the code to add inputs to the form, however I'm having trouble with the individual components (selectTimeInput is a row) actually updating their values.
The onChange in the MultipleDayTimeInput is receiving the correct data, it is just the display that isn't updating. I extremely new to react so I don't know what is causing the display to not update....
I think it is because the SelectTimeInput render function isn't being called because the passed in props aren't being updated, but I'm not sure of the correct way to achieve that.
Thinking about it, does the setState need to be called in the onChange of the MultipleDayTimeInput and the input that changed needs to be removed from the this.state.inputs and readded in order to force the render to fire... this seems a little clunky to me...
When you update the display value of the inputs in state, you need to use this.setState to change the state data and cause a re-render with the new data. Using input.key = value is not the correct way.
Using State Correctly
There are three things you should know about
setState().
Do Not Modify State Directly
For example, this will not re-render a
component:
// Wrong
this.state.comment = 'Hello';
Instead, use setState():
// Correct
this.setState({comment: 'Hello'});
The only place where you
can assign this.state is the constructor.
read more from Facebook directly here
I would actually suggest a little bit of a restructure of your code though. It's not really encouraged to have components as part of your state values. I would suggest having your different inputs as data objects in your this.state.inputs, and loop through the data and build each of the displays that way in your render method. Like this:
suppose you have one input in your this.state.inputs (and suppose your inputs is an object for key access):
inputs = {
1: {
selectedTime: 0:00,
selectedValue: 2
}
}
in your render, do something like this:
render() {
let inputs = Object.keys(this.state.inputs).map((key) => {
let input = this.state.inputs[key]
return (<SelectTimeInput
key={key}
name={'option_' + key}
placeholder={this.props.placeholder}
options={this.props.options}
onChange={this.onChange.bind(this, key)}
timeValue={input.selectedTime}
selectValue={input.selectedValue}
/>)
)}
return (
<div>
<button className="button" onClick={this.onAddClick}><i className="fa fa-plus" /></button>
{ inputs }
</div>
);
}
Notice how we're binding the key on the onChange, so that we know which input to update. now, in your onChange function, you just set the correct input's value with setState:
onChange(event, key) {
this.setState({
inputs: Immutable.fromJS(this.state.inputs).setIn([`${key}`, 'selectedTime'], event.target.value).toJS()
// or
inputs: Object.assign(this.state.inputs, Object.assign(this.state.inputs[key], { timeValue: event.target.value }))
})
}
this isn't tested, but basically this Immutable statement is going to make a copy of this.state.inputs and set the selectedTime value inside of the object that matches the key, to the event.target.value. State is updated now, a re-render is triggered, and when you loop through the inputs again in the render, you'll use the new time value as the timeValue to your component.
again, with the Object.assign edit, it isn't tested, but learn more [here]. 2 Basically this statement is merging a new timeValue value in with the this.state.inputs[key] object, and then merging that new object in with the entire this.state.inputs object.
does this make sense?
I modified the onChange in the MultipleDayTimeInput:
onChange(event) {
const comparisonKey = event.target.name.substring(event.target.name.length - 1);
const input = this.getInputState(comparisonKey);
input.selected = event.target.value;
input.display = this.renderTimeInput(input);
let spliceIndex = -1;
for (let i = 0; i < this.state.inputs.length; i++) {
const matches = inputFilter(comparisonKey)(this.state.inputs[i]);
if (matches) {
spliceIndex = i;
break;
}
}
if (spliceIndex < 0) {
throw 'error updating inputs';
}
this.setState({
inputs: [...this.state.inputs].splice(spliceIndex, 1, input)
});
}
The key points are:
// re render the input
input.display = this.renderTimeInput(input);
// set the state by copying the inputs and interchanging the old input with the new input....
this.setState({
inputs: [...this.state.inputs].splice(spliceIndex, 1, input)
});
Having thought about it though, input is an object reference to the input in the this.state.inputs so actually [...this.states.inputs] would have been enough??

How to update or add to immutable.js List

I'm using Redux and Immutable.js in my React-based project (built on React Boilerplate) and I'm looking for an idiomatic way to update or add to an Immutable.js List.
My current setup. State initially looks like this:
const initialState = fromJS({
accounts: [],
activeAccount: null,
loadedAccounts: [],
});
I have an Immutable Record for an account object:
const account = new Record({
description: '',
id: '',
name: '',
status: '',
});
And my reducer:
function reducer(state = initialState, action) {
switch (action.type) {
case LOAD_ACCOUNT_SUCCESS:
return state
.set('activeAccount', new account(action.account))
default:
return state;
}
}
This works fine - when a LOAD_ACCOUNT_SUCCESS action is fired, activeAccount is updated to the value of action.account.
I can amend this so that every new LOAD_ACCOUNT_SUCCESS action pushes the newly-loaded account data to loadedAccounts instead:
function reducer(state = initialState, action) {
switch (action.type) {
case LOAD_ACCOUNT_SUCCESS:
const loadedAccount = new account(action.account);
return state
.update('loadedAccounts', (loadedAccounts) => loadedAccounts.push(loadedAccount));
default:
return state;
}
}
However, at the moment loading the same account data twice will result in new Records being pushed to my List each time (duplicating data). What I want to do instead is either add action.account to loadedAccounts (as happens now) or update the Record in the List if there is a matching ID. I'm looking at similar questions and the lamentable Immutable.js documentation and I can't see how to do this: no syntax I've tried works as I expect here.
So, what do you need here is nested update.
At first, you have to check your list of loadedAccounts whether it has this account or not.
Secondly, you have to change activeAccount field.
And, lastly, add (or update) account to loadedAccounts.
The caveat here is how you pass account property. If you derive it from somewhere and pass around as a Record, you can just compare by === (or by .equals()), but it seems that it is just a plain javascript object – I'll suppose it later.
In terms of code it would be something like:
// we can do it by different ways, it is just one of them
const listWithLoadedAccounts = state.get('loadedAccounts');
const isAccountAlready = Boolean(
listWithLoadedAccounts.filter(
account => account.get('id') === action.account.id
).size
);
const patchedState = state.set('activeAccount', action.account.id);
return isAccountAlready
? patchedState.updateIn(['loadedAccounts'], list => list.map(account => account.get('id') === account.action.id ? new account(action.account) : account))
: patchedState.updateIn(['loadedAccounts'], list => list.concat(new account(action.account)))
It is not the ideal code, something can be deduplicated, but you get the idea – always use deep merge / update if you need to change nested fields or data structures.
You also can set new field directly, like:
const oldList = state.get('loadedAccounts');
const newList = oldList.concat(action.account);
const patchedState = state.set('loadedAccounts', newList);
But I personally find that it is not that flexible and also not consistent, because it is quite common operation to perform deep merge.
i hope this example will help, i am creating a new immutable list and first performing an update and then adding a new element. i am not passing the object which i want to replace with, but you can also pass your existing object, Also in update method you have access to current item
class Test {
a = null;
b = null;
constructor(a,b){
this.a=a;
this.b=b;
}
}
$("#test").html("");
function logme(item){
$("#test").append("<br/>"+JSON.stringify(item));
}
function logmeNewLine(){
$("#test").append("<br/>");
}
function listAddUpadte(key){
var index= list.get('data').findIndex(listing => {
return listing.a === key;
});
logme(' found index (-1 for not found) : ' + index);
if(index >= 0){
logme("upadte");
list = list.set("data",list.get("data").update(index,function(item){
return new Test(key,"go");
}));
}else {
logme("add");
list = list.set("data",list.get("data").push(new Test(key,"go")));
}
list.get('data').forEach(item=>{
logme(item);
});
}
var list = Immutable.fromJS({
"data":[new Test(6,"abhi"),new Test(4,"raj"),new Test(1,"ajay")]
});
logme("intial data");
list.get('data').forEach(item=>{
logme(item);
});
logmeNewLine();
logme("testing replace with a = 4 ")
logmeNewLine();
listAddUpadte(4);
logmeNewLine();
logme("testing add with a = 8 ")
logmeNewLine();
listAddUpadte(8);
logmeNewLine();
logmeNewLine();
logmeNewLine();
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.7.2/immutable.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="test"></div>
Since this is the top search result for what is in the title of the question
https://immutable-js.github.io/immutable-js/docs/#/List/push
An example of how to add to a list using immutable.js:
let oldList = List([ 1, 2, 3, 4 ])
let newList = oldList.push(5)
https://immutable-js.com/docs/v4.0.0/List/#insert()
insert()
Returns a new List with value at index with a size 1 more than this List. Values at indices above index are shifted over by 1.
insert(index: number, value: T): List
Discussion
This is synonymous with list.splice(index, 0, value).
List([ 0, 1, 2, 3, 4 ]).insert(6, 5)
// List [ 0, 1, 2, 3, 4, 5 ]

How to manage state without using Subject or imperative manipulation in a simple RxJS example?

I have been experimenting with RxJS for two weeks now, and although I love it in principle I just cannot seem to find and implement the correct pattern for managing state. All articles and questions appear to agree:
Subject should be avoided where possible in favor of just pushing state through via transformations;
.getValue() should be deprecated entirely; and
.do should perhaps be avoided except for DOM manipulation?
The problem with all such suggestions is that none of the literature appears to directly say what you should be using instead, besides "you'll learn the Rx way and stop using Subject".
But I cannot find a direct example anywhere that specifically indicates the correct way to perform both additions and removals to a single stream/object, as the consequence of multiple other stream inputs, in a stateless and functional manner.
Before I get pointed in the same directions again, problems with uncovered literature are:
The Introduction to Reactive Programming You've been missing: great starting text, but does not specifically address these questions.
The TODO example for RxJS comes with React and involves explicit manipulation of Subjects as proxies for React Stores.
http://blog.edanschwartz.com/2015/09/18/dead-simple-rxjs-todo-list/ : explicitly uses a state object for addition and removal of items.
My perhaps 10th rewrite of the standard TODO follows - My prior iterations covered include:
starting with a mutable 'items' array - bad as state is explicit and imperatively managed
using scan to concatenate new items to an addedItems$ stream, then branching another stream where the removed items were deleted - bad as the addedItems$ stream would grow indefinitely.
discovering BehaviorSubjectand using that - seemed bad since for each new updatedList$.next() emission, it requires the previous value to iterate, meaning that Subject.getValue() is essential.
trying to stream the result of the inputEnter$ addition events into filtered removal events - but then every new stream creates a new list, and then feeding that into the toggleItem$ and toggleAll$ streams means that each new stream is dependent on the previous, and so causing one of the 4 actions (add, remove, toggle item or toggle all) requires the whole chain to be unnecessarily run through again.
Now I have come full circle, where I am back to using both Subject (and just how is it supposed to be successively iterated upon in any way without using getValue()?) and do, as show below. Myself and my colleague agree this is the clearest way, yet it of course seems the least reactive and most imperative. Any clear suggestions on the correct way for this would be much appreciated!
import Rx from 'rxjs/Rx';
import h from 'virtual-dom/h';
import diff from 'virtual-dom/diff';
import patch from 'virtual-dom/patch';
const todoListContainer = document.querySelector('#todo-items-container');
const newTodoInput = document.querySelector('#new-todo');
const todoMain = document.querySelector('#main');
const todoFooter = document.querySelector('#footer');
const inputToggleAll = document.querySelector('#toggle-all');
const ENTER_KEY = 13;
// INTENTS
const inputEnter$ = Rx.Observable.fromEvent(newTodoInput, 'keyup')
.filter(event => event.keyCode === ENTER_KEY)
.map(event => event.target.value)
.filter(value => value.trim().length)
.map(value => {
return { label: value, completed: false };
});
const inputItemClick$ = Rx.Observable.fromEvent(todoListContainer, 'click');
const inputToggleAll$ = Rx.Observable.fromEvent(inputToggleAll, 'click')
.map(event => event.target.checked);
const inputToggleItem$ = inputItemClick$
.filter(event => event.target.classList.contains('toggle'))
.map((event) => {
return {
label: event.target.nextElementSibling.innerText.trim(),
completed: event.target.checked,
};
})
const inputDoubleClick$ = Rx.Observable.fromEvent(todoListContainer, 'dblclick')
.filter(event => event.target.tagName === 'LABEL')
.do((event) => {
event.target.parentElement.classList.toggle('editing');
})
.map(event => event.target.innerText.trim());
const inputClickDelete$ = inputItemClick$
.filter(event => event.target.classList.contains('destroy'))
.map((event) => {
return { label: event.target.previousElementSibling.innerText.trim(), completed: false };
});
const list$ = new Rx.BehaviorSubject([]);
// MODEL / OPERATIONS
const addItem$ = inputEnter$
.do((item) => {
inputToggleAll.checked = false;
list$.next(list$.getValue().concat(item));
});
const removeItem$ = inputClickDelete$
.do((removeItem) => {
list$.next(list$.getValue().filter(item => item.label !== removeItem.label));
});
const toggleAll$ = inputToggleAll$
.do((allComplete) => {
list$.next(toggleAllComplete(list$.getValue(), allComplete));
});
function toggleAllComplete(arr, allComplete) {
inputToggleAll.checked = allComplete;
return arr.map((item) =>
({ label: item.label, completed: allComplete }));
}
const toggleItem$ = inputToggleItem$
.do((toggleItem) => {
let allComplete = toggleItem.completed;
let noneComplete = !toggleItem.completed;
const list = list$.getValue().map(item => {
if (item.label === toggleItem.label) {
item.completed = toggleItem.completed;
}
if (allComplete && !item.completed) {
allComplete = false;
}
if (noneComplete && item.completed) {
noneComplete = false;
}
return item;
});
if (allComplete) {
list$.next(toggleAllComplete(list, true));
return;
}
if (noneComplete) {
list$.next(toggleAllComplete(list, false));
return;
}
list$.next(list);
});
// subscribe to all the events that cause the proxy list$ subject array to be updated
Rx.Observable.merge(addItem$, removeItem$, toggleAll$, toggleItem$).subscribe();
list$.subscribe((list) => {
// DOM side-effects based on list size
todoFooter.style.visibility = todoMain.style.visibility =
(list.length) ? 'visible' : 'hidden';
newTodoInput.value = '';
});
// RENDERING
const tree$ = list$
.map(newList => renderList(newList));
const patches$ = tree$
.bufferCount(2, 1)
.map(([oldTree, newTree]) => diff(oldTree, newTree));
const todoList$ = patches$.startWith(document.querySelector('#todo-list'))
.scan((rootNode, patches) => patch(rootNode, patches));
todoList$.subscribe();
function renderList(arr, allComplete) {
return h('ul#todo-list', arr.map(val =>
h('li', {
className: (val.completed) ? 'completed' : null,
}, [h('input', {
className: 'toggle',
type: 'checkbox',
checked: val.completed,
}), h('label', val.label),
h('button', { className: 'destroy' }),
])));
}
Edit
In relation to #user3743222 very helpful answer, I can see how representing state as an additional input can make a function pure and thus scan is the best way to represent a collection evolving over time, with a snapshot of its previous state up to that point as an additional function parameter.
However, this was already how I approached my second attempt, with addedItems$ being a scanned stream of inputs:
// this list will now grow infinitely, because nothing is ever removed from it at the same time as concatenation?
const listWithItemsAdded$ = inputEnter$
.startWith([])
.scan((list, addItem) => list.concat(addItem));
const listWithItemsAddedAndRemoved$ = inputClickDelete$.withLatestFrom(listWithItemsAdded$)
.scan((list, removeItem) => list.filter(item => item !== removeItem));
// Now I have to always work from the previous list, to get the incorporated amendments...
const listWithItemsAddedAndRemovedAndToggled$ = inputToggleItem$.withLatestFrom(listWithItemsAddedAndRemoved$)
.map((item, list) => {
if (item.checked === true) {
//etc
}
})
// ... and have the event triggering a bunch of previous inputs it may have nothing to do with.
// and so if I have 400 inputs it appears at this stage to still run all the previous functions every time -any- input
// changes, even if I just want to change one small part of state
const n$ = nminus1$.scan...
The obvious solution would be to just have items = [], and manipulate it directly, or const items = new BehaviorSubject([]) - but then the only way to iterate on it appears to be using getValue to expose the previous state, which Andre Stalz (CycleJS) has commented on in the RxJS issues as something that shouldn't really be exposed (but again, if not, then how is it usable?).
I guess I just had an idea that with streams, you weren't supposed to use Subjects or represent anything via a state 'meatball', and in the first answer I'm not sure how this doesn't introduce mass chained streams which are orphaned/grow infinitely/have to build on each other in exact sequence.
I think you already found a good example with : http://jsbin.com/redeko/edit?js,output.
You take issue with the fact that this implementation
explicitly uses a state object for addition and removal of items.
However, thas is exactly the good practice you are looking for. If you rename that state object viewModel for example, it might be more apparent to you.
So what is state?
There will be other definitions but I like to think of state as follows:
given f an impure function, i.e. output = f(input), such that you can have different outputs for the same input, the state associated to that function (when it exists) is the extra variable such that f(input) = output = g(input, state) holds and g is a pure function.
So if the function here is to match an object representing a user input, to an array of todo, and if I click add on a todo list with already have 2 todos, the output will be 3 todos. If I do the same (same input) on a todo list with only one todo, the output will be 2 todos. So same input, different outputs.
The state here that allows to transform that function into a pure function is the current value of the todo array. So my input becomes an add click, AND the current todo array, passed through a function g which give a new todo array with a new todo list. That function g is pure. So f is implemented in a stateless way by making its previously hidden state explicit in g.
And that fits well with functional programming which revolves around composing pure functions.
Rxjs operators
scan
So when it comes to state management, with RxJS or else, a good practice is to make state explicit to manipulate it.
If you turn the output = g(input, state) into a stream, you get On+1 = g(In+1, Sn) and that's exactly what the scan operator does.
expand
Another operator which generalizes scan is expand, but so far I had very little use of that operator. scan generally does the trick.
Sorry for the long and mathy answer. It took me a while to get around those concepts and that's the way I made them understandable for me. Hopefully it works for you too.

Categories

Resources