Last active
March 15, 2017 09:28
-
-
Save orbitbot/292059bbe30400234ba4dc3ebb92ec51 to your computer and use it in GitHub Desktop.
the Mithril dialogues - insightful exchanges in the chatroom
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
dontwork @dontwork Feb 07 12:22 | |
export function getUserFormData() { | |
var form = Object.assign({}, store.getState().users.form) | |
return form | |
} | |
that should return a copy of the form object right | |
Magnus Leo @magnusleo Feb 07 12:37 | |
Yes. | |
Object.assign is not available in IE11 if you must support that. | |
We use http://ramdajs.com/docs/#merge instead. | |
James Forbes @JAForbes Feb 07 12:42 | |
@dontwork a shallow copy | |
dontwork @dontwork Feb 07 12:46 | |
so it wont clone objects within form? | |
James Forbes @JAForbes Feb 07 12:46 | |
yeah just top level properties | |
Pierre-Yves Gérardy @pygy Feb 07 12:47 | |
@JAForbes Neat demo :grinning: Be careful with your component helper, though, it only works because it is called once, it won't cut it in views. | |
dontwork @dontwork Feb 07 12:47 | |
so if theres a object on form.user - what is user? | |
just null? | |
James Forbes @JAForbes Feb 07 12:47 | |
I was planning on only calling it on export, is that still ok @pygy ? | |
e.g. export component(Main) | |
@dontwork a reference to the same object as the source | |
dontwork @dontwork Feb 07 12:48 | |
boom | |
thanks | |
thats my error | |
James Forbes @JAForbes Feb 07 12:48 | |
ah great! | |
Pierre-Yves Gérardy @pygy Feb 07 12:48 | |
Then it needs to be a singleton, because vnode.tag points to the component object, which is used as prototype fot the state of all instances. | |
dontwork @dontwork Feb 07 12:49 | |
so in terms of good software design is it better to code around the restrictions of object.assign or just move to ramda for stuff like this | |
Pierre-Yves Gérardy @pygy Feb 07 12:49 | |
You shouldn't use Object.assign(node.tag, ...) if you want a non-singleton component | |
James Forbes @JAForbes Feb 07 12:50 | |
oh really?! | |
far out | |
that's pretty powerful/scary | |
@dontwork I'm biased :grinning: | |
"coding around" things is never a good idea in my books | |
@pygy writing to state ok? | |
Pierre-Yves Gérardy @pygy Feb 07 12:51 | |
Yes | |
dontwork @dontwork Feb 07 12:51 | |
but i guess my question is 'oh, object.assign doesnt do exactly what i meant it to, so should I assume I need to use something else or should I start questioning the structure of my data' | |
James Forbes @JAForbes Feb 07 12:52 | |
Well, staying away from nested structures as a rule can be useful for a variety of reasons, but its all contextual. Sometimes nested data is really useful | |
Thanks @pygy, that's really interesting | |
dontwork @dontwork Feb 07 12:54 | |
well it seems necessary to have a bit of nest here because i am modelling form data like so form: {user: {name, surname}, validationErrors: {}} | |
dontwork @dontwork Feb 07 13:04 | |
which ramda function would be correct for a deepclone | |
James Forbes @JAForbes Feb 07 13:08 | |
clone is a deep copy function, evolve deeply traverses an object and calls a transformation function on each key, merge is like Object.assign, but you don't need to supply the first arg of the empty object. | |
Probably a little bit beyond what your after, but its on topic: https://github.com/DrBoolean/immutable-ext | |
dontwork @dontwork Feb 07 13:09 | |
not sure i know what that link is about | |
im skeptical of continuing to use this nested object now but i dont know what the alternative would look lik | |
James Forbes @JAForbes Feb 07 13:11 | |
I think just replace your Object.assign calls with clone, if you hit perf issues later come back and I'll explain the link | |
dontwork @dontwork Feb 07 13:12 | |
the rexux docs suggest deep cloning shouldnt be necessary which suggests I should have different reducers i guess? but then what, have one for forms, one for errors, one per resource? | |
James Forbes @JAForbes Feb 07 13:13 | |
yeah if your doing something like redux, cloning isn't necessary | |
dontwork @dontwork Feb 07 13:13 | |
so im laying out my data wrong then | |
James Forbes @JAForbes Feb 07 13:13 | |
but that's because you never have a reference to the state object, you never mutate it, you just return a new one | |
are you doing redux? | |
dontwork @dontwork Feb 07 13:14 | |
i am doing redux | |
but when i access the state you need to clone it on the way out, make edits then clone it back in | |
e.g if i want to access a user from a list of users in the store, it needs to lose its reference | |
James Forbes @JAForbes Feb 07 13:15 | |
you only need to lose the reference if your are going to mutate it, but in redux you don't mutate anything ever, you just return a new user, or a new users list | |
its ok if there's some shared references, because the only probably with references is if they're going to be mutated, but that's not a problem with that pattern | |
show me one of your reducers where your editing | |
dontwork @dontwork Feb 07 13:16 | |
its not a reducer | |
its getting access to the store | |
because i have an object like | |
form: {user: {name, surname}, validationErrors: {}} | |
and use is keeping its reference | |
but I want to update the form user in the store | |
but when i change the variables im actually editing the store version | |
James Forbes @JAForbes Feb 07 13:17 | |
so this may sound radical, but never mutate the store ever, just return a new store in a reducer. | |
If you find yourself mutating it, instead dispatch an action to make that edit | |
dontwork @dontwork Feb 07 13:17 | |
yeah i guess youre right | |
James Forbes @JAForbes Feb 07 13:18 | |
and its ok to do shallow clones in the reducer, the key thing is, your never mutating it, your just returning a new object with some shared references, but because their references are essentially read only, its ok | |
dontwork @dontwork Feb 07 13:18 | |
but id rather pass the action a whole form object and replace the existing one | |
instead of passing it a single property | |
James Forbes @JAForbes Feb 07 13:18 | |
the reducer should be receiving two things, the existing state object, and an action | |
dontwork @dontwork Feb 07 13:18 | |
i know | |
James Forbes @JAForbes Feb 07 13:19 | |
so your not passing it a single property right? | |
dontwork @dontwork Feb 07 13:19 | |
one sec | |
atm im doing this | |
function updateUserState(user){ | |
return function (event){ | |
const field = event.target.name | |
user[field] = event.target.value | |
console.log(getUserFormData()) | |
store.dispatch(setFormUser(user)) | |
} | |
} | |
James Forbes @JAForbes Feb 07 13:19 | |
yeah avoid that | |
dontwork @dontwork Feb 07 13:19 | |
user though is a reference | |
and replace it with what? | |
James Forbes @JAForbes Feb 07 13:21 | |
function update(state, action){ | |
if( action.action == 'UPDATE_USER_STATE' ){ | |
const { property, value } = action | |
return Object.assign( | |
{} | |
, state | |
, { user: Object.assign( {}, user, { [property]: value } | |
} | |
) | |
} | |
return state | |
} | |
function updateUserState({ target: { name, value }}){ | |
store.dispatch( | |
{ action: 'UPDATE_USER_STATE', property: name, value } | |
) | |
} | |
dontwork @dontwork Feb 07 13:22 | |
and what is the user param in your updateUserState? that is the user i have gotten out of the store but because its nested inside a form object it is a reference not a copy | |
that is the same as my code :stuck_out_tongue: | |
ive gone off the rails somewhere havent i | |
https://gist.github.com/dontwork/5b91e781d6bcb435782a1752c0d8c32 | |
https://gist.github.com/dontwork/5b91e781d6bcb435782a1752c0d8c326 | |
thats my reducer | |
James Forbes @JAForbes Feb 07 13:26 | |
updated it | |
dontwork @dontwork Feb 07 13:27 | |
thanks that is helpful | |
what do you make of the current reducer code? | |
do you think there should be a separate reducer for the formData | |
James Forbes @JAForbes Feb 07 13:30 | |
nah thats good | |
dontwork @dontwork Feb 07 13:31 | |
yh? | |
James Forbes @JAForbes Feb 07 13:31 | |
yeah | |
dontwork @dontwork Feb 07 13:31 | |
im still doing incorrect object.assigns though | |
James Forbes @JAForbes Feb 07 13:31 | |
yep it all looks good | |
dontwork @dontwork Feb 07 13:31 | |
i like that you have done the store.dispatch inside your action creator, I think i will adopt that | |
James Forbes @JAForbes Feb 07 13:31 | |
ok so brief aside... | |
dontwork @dontwork Feb 07 13:31 | |
i dont like importing store everywhere | |
go on... | |
James Forbes @JAForbes Feb 07 13:32 | |
you'll probably find as you continue down the redux path that there's some pretty common operations, and your writing similar code often, e.g. add a uniq item to a list, or merge these two versions of something, or remove an item from a list on some nested property, or out of 2 things pick 1 of them based on some condition | |
it gets old | |
it turns out, these operations are able to be expressed in mathematics, and some of them under familiar names, e.g. filter, concat, map | |
dontwork @dontwork Feb 07 13:34 | |
ok | |
James Forbes @JAForbes Feb 07 13:34 | |
that immutable-ext library is a series of data structures that change the behaviour of those operations, so you don't need to specify them all the time | |
dontwork @dontwork Feb 07 13:34 | |
are you able to provide an example? | |
https://github.com/dontwork/mithril-start/tree/master/src/data/users | |
James Forbes @JAForbes Feb 07 13:34 | |
in addition, its built on immutable-js, so anytime you set a property, it returns a completely new object, it automatically clones. Its actually a lot more performant for reasons I won't go into. | |
dontwork @dontwork Feb 07 13:34 | |
theres some code i current have | |
James Forbes @JAForbes Feb 07 13:35 | |
ok so | |
case types.LOAD_USER_SUCCESS: | |
return [ | |
...state.filter(user => user.id !== action.user.id), | |
Object.assign({}, action.user) | |
] | |
perfect example | |
Imagine you had a Set, and you could define via a function what makes it uniq, in this case, its an id | |
All your really want to do is concat to sets together | |
users.concat( UserSet(action.user) ) | |
Pierre-Yves Gérardy @pygy Feb 07 13:36 | |
@JAForbes need to go. I have two closed PRs related to "how you use Mithril": #1293, which was superseeded by #1339... It may be time to revive the latter... | |
James Forbes @JAForbes Feb 07 13:37 | |
@pygy thanks I'll dive in :smiley: | |
@dontwork So, don't worry about it now, but that's what it is. And that's what that library is all about, and when you grok it, its pretty amazing for Redux. Those datastructures are called Semigroups. Which is just a data structure that has a concat method that obeys some laws. | |
There's a brilliant demo of it somewhere, I'll find the video its like 5mins long | |
But really all we ever want to do in a reducer is concat two data types. When you get right down to it. Its just the behaviour of concat that changes. | |
James Forbes @JAForbes Feb 07 13:42 | |
There's nothing wrong with doing it manually, but I think its cool to now there's always some other pattern on the horizon @dontwork | |
dontwork @dontwork Feb 07 13:43 | |
so what is userSet? | |
case types.SET_FORM_USER: | |
return Object.assign( | |
{}, | |
state, | |
{ form: { | |
validationErrors: state.form.validationErrors, | |
user: action.user | |
}} | |
) | |
the above action is still incorrect right? because both those properties values should be getting object.assigned? | |
James Forbes @JAForbes Feb 07 13:49 | |
no its fine in that case | |
dontwork @dontwork Feb 07 13:50 | |
oh | |
youve used [property] as a property name? | |
computed property names? is that supported? | |
James Forbes @JAForbes Feb 07 13:51 | |
yep | |
ramda has a function called objOf that does the same thing objOf( key, value ) => {[key]: value} | |
I sometimes use that instead | |
dontwork @dontwork Feb 07 13:54 | |
ok thanks | |
this is all great btw | |
so would you do store.dispatch in your action creators? | |
none of the 'idiomatic' redux guides do this | |
perhaps to keep store out of action namespace | |
James Forbes @JAForbes Feb 07 13:58 | |
well, I kind of skipped the action creator step didn't I? By just defining the action manually inline. Apologies, I'm no redux expert and I might be mixing terminology up. | |
dontwork @dontwork Feb 07 13:59 | |
function updateUserState({ target: { name, value }}){ | |
store.dispatch( | |
{ action: 'UPDATE_USER_STATE', property: name, value } | |
) | |
} | |
you kind of mixed action creators with store.dispatch | |
that function as an 'action creator' would be" | |
function updateUserState({ target: { name, value }}){ | |
return { action: 'UPDATE_USER_STATE', property: name, value } | |
} | |
but i dont get why youd do that because then you need to import store into all the files you run actions from | |
James Forbes @JAForbes Feb 07 14:02 | |
I think you should do whatever redux is suggesting, I only know the concepts because I use similar approaches but I don't use redux itself, so I'm not going to be helpful for idiomatic stuff | |
dontwork @dontwork Feb 07 14:02 | |
I dont think anyone would argue that idiomatic redux is perhaps a little verbose | |
James Forbes @JAForbes Feb 07 14:02 | |
in general though, I'm anti putting everything in separate files by default | |
dontwork @dontwork Feb 07 14:09 | |
@JAForbes youre update function contains a user variable not defined anywhere | |
James Forbes @JAForbes Feb 07 14:10 | |
I guess state.user? Its psuedo code | |
Patrik Johnson @orbitbot Feb 07 14:28 | |
I dont think anyone would argue that idiomatic redux is perhaps a little verbose | |
What do you mean, "perhaps" ? | |
import store into all the files you run actions from | |
The app I'm looking at (well, aside from the Android stuff atm) does this AFAICT, but different stores depending on what state is being tracked + modified | |
dontwork @dontwork Feb 07 14:34 | |
different stores? | |
Patrik Johnson @orbitbot Feb 07 14:37 | |
yup, different stores, different state provided by "services" that handle disparate functionality | |
or, separate, even | |
dontwork @dontwork Feb 07 14:38 | |
sounds bad | |
http://redux.js.org/docs/basics/Actions.html | |
this page says dont dispatch from within action creators, but you can make another function which dispatches them and does something identitical | |
Patrik Johnson @orbitbot Feb 07 14:39 | |
oh, wait, I think I misunderstood something. | |
James Forbes @JAForbes Feb 07 14:39 | |
that's so you can test an "action creator", but if an "action creator" is just: { type: 'SOMETHING', x: 1, y: 2} does it need to be tested? | |
Patrik Johnson @orbitbot Feb 07 14:39 | |
not a redux expert either, I just play one complain about it at work | |
dontwork @dontwork Feb 07 14:39 | |
@JAForbes surely its not a crime to dispatch actions from the action creators? | |
yet it explicitly says not too, but doesnt say why | |
James Forbes @JAForbes Feb 07 14:40 | |
I think an "action creator" is a fancy word for a function that returns an object, otherwise known as a constructor | |
it shouldn't dispatch anything | |
it should just create an object | |
dontwork @dontwork Feb 07 14:41 | |
but why when the object its creating can only be used as a store.dispatch parameter | |
James Forbes @JAForbes Feb 07 14:41 | |
putting logic in an "action creator" is where the real problem is right, that's when tests are needed, but having logic in action goes against the whole point of having actions | |
dontwork @dontwork Feb 07 14:41 | |
can you explain? | |
i guess ill just use the 'bound' action creator pattern they mention | |
James Forbes @JAForbes Feb 07 14:42 | |
who's to say that it will only be called by the store, maybe you'll send actions over the wire? Maybe you'll batch them, and filter them, or aggregate them to optimize your dispatch | |
dontwork @dontwork Feb 07 14:43 | |
i mean possibly, im yet to encounter that on my redux journey | |
well im managing the form in redux now | |
James Forbes @JAForbes Feb 07 14:44 | |
In FP, constructors don't do anything other than construct. And that makes sense, because in FP your always deferring work to some other process. I think the guide is trying to guide the reader away from conflating logic and data | |
dontwork @dontwork Feb 07 14:44 | |
thanks for your help | |
James Forbes @JAForbes Feb 07 14:44 | |
but if your comfortable, its ok to do whatever | |
dontwork @dontwork Feb 07 14:45 | |
you understand my rough setup right? | |
where would you define form validation functions? | |
custom ones | |
James Forbes @JAForbes Feb 07 14:47 | |
you could do it a few ways, one way could be: first have an action that describes something is invalid, then have a reducer to generate/aggregate the error messages, then render them | |
dontwork @dontwork Feb 07 14:49 | |
so lets say I had a single action for validateUserForm, which intends to have items added to the list of validationErrors, the action would loop over all values and do manual validation within the function? | |
James Forbes @JAForbes Feb 07 14:51 | |
another way, could be making your store types include the notion of error. | |
E.g. say you have a password on your store, for a sign up. | |
It might look like: | |
{ | |
password: null | |
} | |
And you want to validate that its not null. | |
You might have a reducer that checks if its not null, and if it is, dispatches a validation action etc. But what if we didn't use null. | |
What if we used a tuple, where the first item is the error, and the second is the value. Kind of like a synchronous Promise | |
{ | |
password: ['A password is required before submitting', null] // invalid value | |
} | |
password: [null,'ABC123'] //valid value | |
And there's no reason the tuple has to be just strings, you could store more context on either side. | |
The you can probably imagine your conditional rendering code. | |
function view(store){ | |
const [err, password] = store.password | |
return err ? m(... ) : m( ... ) | |
} | |
James Forbes @JAForbes Feb 07 14:57 | |
@dontwork its wide open, its up to you | |
dontwork @dontwork Feb 07 14:59 | |
Yeah I guess the thing that makes it hard to figure out the best way is that, it'd be nice to keep the validation logic out of the part that's going to be going to and from the server. I'll have a read | |
Barney Carroll @barneycarroll Feb 07 15:17 | |
I guess you'd try and seliarize your tuples, taking the value at index 1 | |
dontwork @dontwork Feb 07 15:18 | |
but what if a field is required, also requires 2 words and has a min length | |
Hitesh Joshi @hiteshjoshi Feb 07 15:24 | |
how do we do data binding in version 1? Earlier I used to send it to m.withAttr and it would just work, not anymore. Even not when i redraw. | |
Barney Carroll @barneycarroll Feb 07 15:25 | |
It's not about redraw, @hiteshjoshi | |
Now data binding is done simply by assignment | |
If you want to bring back m.prop, you can include stream separately | |
http://mithril.js.org/stream.html | |
Hitesh Joshi @hiteshjoshi Feb 07 15:27 | |
I am using streams. | |
The only thing is, it wont update the DOM when value changes. | |
Barney Carroll @barneycarroll Feb 07 15:28 | |
Streams are just a way of storing data that can change over time | |
Redraw is a separate thing | |
Hitesh Joshi @hiteshjoshi Feb 07 15:28 | |
Now data binding is done simply by assignment | |
I can bind to a value, but how do we update dom? | |
assignment and storing values isnt a problem. Problem is reflecting that on dom. | |
Barney Carroll @barneycarroll Feb 07 15:32 | |
https://jsbin.com/mumuqi/edit?js,output | |
In the above demo, it works out of the box, because whenever an event handler bound with Mithril (in this case oninput) fires, Mithril will redraw automatically | |
Route changes & m.request resolutions also fire redraws automatically | |
If you want to redraw data as a result of an action that doesn't redraw automatically, simply call m.redraw at the appropriate time. | |
Here's another example | |
https://jsbin.com/zisada/edit?js,output | |
Barney Carroll @barneycarroll Feb 07 15:39 | |
As you can see changing the input no longer automatically updates the DOM, because I set e.redraw = false in the event | |
Then outside of the component code I tell Mithril to redraw every time the document is clicked, instead | |
Fred Daoud @foxdonut Feb 07 15:43 | |
@JAForbes still around? would love to see that video you mentioned regarding immutable-ext, looks quite interesting to me. | |
Barney Carroll @barneycarroll Feb 07 15:47 | |
yeah me too | |
@hiteshjoshi can you reproduce your problem in a jsbin or show us some code, maybe there's a reference problem or something? | |
James Forbes @JAForbes Feb 07 16:01 | |
@foxdonut ah completely forgot! | |
Barney Carroll @barneycarroll Feb 07 16:07 | |
You better not be forgetting again @JAForbes | |
Hitesh Joshi @hiteshjoshi Feb 07 16:08 | |
@barneycarroll The problem is with nested nodes. Let me reproduce it. | |
Barney Carroll @barneycarroll Feb 07 16:08 | |
Oh | |
@hiteshjoshi yeah that can be nasty | |
The rule of thumb with streams is not to execute them until the last minute | |
Hitesh Joshi @hiteshjoshi Feb 07 16:10 | |
I see. | |
James Forbes @JAForbes Feb 07 16:13 | |
Watch this first: https://egghead.io/lessons/javascript-combining-things-with-semigroups | |
Then watch the video that immediately follows it ( https://egghead.io/lessons/javascript-semigroup-examples ) | |
The 1st video explains and demos a few different kinds of semigroups. The 2nd video is a great practical example of merging two user accounts where each property of the object needs to be merged in different ways. | |
Worth watching the whole course, but those 2 videos are relevant to redux style UI paradigms ( and programming in general really, how often do you need to combine/merge/add/concat/assign things? ). | |
CC @foxdonut @dontwork @barneycarroll | |
Its like 7 mins all up | |
Hitesh Joshi @hiteshjoshi Feb 07 16:14 | |
Sorry, it was my bad! I was trying to update nested components, the nodes changes for components. | |
Barney Carroll @barneycarroll Feb 07 16:14 | |
Ah! | |
Could you post a simplified example? That kind of stuff crops up fairly often it might be useful to others | |
Thx @JAForbes | |
Hitesh Joshi @hiteshjoshi Feb 07 16:16 | |
Yes, ok | |
James Forbes @JAForbes Feb 07 16:19 | |
@barneycarroll my pleasure | |
hope it is interesting | |
Barney Carroll @barneycarroll Feb 07 16:20 | |
Always been too lazy to actually watch an egghead course | |
I figure practical data structures in Javascript might just hold my attention :) | |
Fred Daoud @foxdonut Feb 07 16:31 | |
@JAForbes thanks! | |
James Forbes @JAForbes Feb 07 16:31 | |
:sparkles: | |
let me know what you think @foxdonut | |
Fred Daoud @foxdonut Feb 07 16:44 | |
will do James! | |
dontwork @dontwork Feb 07 16:46 | |
thanks @JAForbes | |
James Forbes @JAForbes Feb 07 16:46 | |
Hope it helps on your redux journey! | |
dontwork @dontwork Feb 07 16:46 | |
I have a Q. You know the action you created that you pass an event, (this is something i have struggled with before) but what if the property that needs to be changed is nested in another property | |
like user.address.street | |
and i need to changer street | |
Barney Carroll @barneycarroll Feb 07 16:47 | |
:point_up: February 7, 2017 2:46 PM that's unnecessarily reductive IMO :laughing: | |
James Forbes @JAForbes Feb 07 16:47 | |
ha | |
For starters you could use something like R.assocPath | |
which will return a copy of the object while associating a value to that path in the copy | |
dontwork @dontwork Feb 07 16:49 | |
theres no way of doing it with computed property i take it | |
can i pass it eg address.street and it does it | |
James Forbes @JAForbes Feb 07 16:50 | |
https://goo.gl/YjODbh | |
computed paths? Not in js no | |
maybe there's some hack I'm not aware of with destructuring and object spread? haha not sure | |
Barney Carroll @barneycarroll Feb 07 16:51 | |
Hang on, computing the property is just a matter of partial application, surely? | |
If you do it in 2 steps I'm sure that's possible | |
dontwork @dontwork Feb 07 16:52 | |
im wanting to access something in the next level down barney | |
oh | |
basically redux takes the 'name' of the field and uses that as a property | |
Barney Carroll @barneycarroll Feb 07 16:52 | |
Yes | |
dontwork @dontwork Feb 07 16:52 | |
so i was wondering if there was a non-hacky way to cater to address.street | |
Barney Carroll @barneycarroll Feb 07 16:52 | |
James, you were talking to me about this the other day | |
Lenses | |
James Forbes @JAForbes Feb 07 16:53 | |
yeah lenses are great, I just didn't want to wear out my welcome | |
Barney Carroll @barneycarroll Feb 07 16:53 | |
Your welcome or your patience? Haha | |
dontwork @dontwork Feb 07 16:54 | |
currently have this: | |
const { property, value } = action | |
return Object.assign({}, | |
state, { | |
form: { | |
user: Object.assign({}, | |
state.form.user, { | |
[property]: value | |
} | |
) | |
} | |
} | |
) | |
Patrik Johnson @orbitbot Feb 07 16:57 | |
jeez. blog-posters' "Top JS to learn in 2017" lists considered harmful... | |
Barney Carroll @barneycarroll Feb 07 16:57 | |
Haha did you find out the hard way? | |
James Forbes @JAForbes Feb 07 16:57 | |
welcome! | |
Patrik Johnson @orbitbot Feb 07 16:59 | |
nah, was following some links from stuff posted here earlier today and got quite overwhelmed by the size and "optional/compulsory" separation in this post: https://medium.com/javascript-scene/top-javascript-frameworks-topics-to-learn-in-2017-700a397b711#.j3tnrjuxl | |
dontwork @dontwork Feb 07 16:59 | |
just blogspam | |
Patrik Johnson @orbitbot Feb 07 17:00 | |
"Anything not marked with a * should be learned"... and the contents of the "suggested" linked content is probably more than I read on JS in a year... | |
Barney Carroll @barneycarroll Feb 07 17:00 | |
Yeah, this is lazy | |
James Forbes @JAForbes Feb 07 17:00 | |
I wanted to write a blog post about lenses + mithril instead of just launching into this, so for now I'll just post a little usage example without going into the why/what/etc | |
var state = { | |
user: { | |
address: { | |
street: '' | |
} | |
} | |
} | |
const street = | |
lensPath(['user', 'address', 'street']) | |
state = set( street, '31 Example St', state ) | |
//=> {"user": {"address": {"street": "31 Example St"}}} | |
state = over( street, R.toUpper, state ) | |
//=> {"user": {"address": {"street": "31 EXAMPLE ST"}}} | |
view( street, state ) | |
//=> "31 EXAMPLE ST" | |
Live Example | |
but high level, a lens is kind of like a prop except the state is external to the get/set functionality | |
a lens is named, a lens, because it focuses on a particular subset of a larger structure, the user of the lens requires no knowledge of what that lens focuses on, so you have separation of concerns, you pass the lens around with the state, and a view, or a reducer, or whatever can operate on the state, (purely) without needing to know anything about the state's structure. | |
dontwork @dontwork Feb 07 17:03 | |
is this concept useful for this problem though? If i just did this without asking I would split the name on . and use the second item in the array as the secon property down | |
James Forbes @JAForbes Feb 07 17:06 | |
Because its ramda, as always, the state is the last argument. So you can create functions by leaving off the final argument. E.g. | |
Say you want to create a function that inserts a child into a mithril vnode children property: | |
var vnode = { | |
children: ['world'] | |
} | |
const children = | |
lensProp('children') | |
const prependChild = child => over( children, R.concat([child]) ) | |
const prependHello = prependChild( 'hello' ) | |
prependHello(vnode) | |
//=> {"children": ["hello", "world"]} | |
Live Example | |
@dontwork did you see the sample I posted that assigns to user.address.street ? | |
Should I paste inline? | |
dontwork @dontwork Feb 07 17:08 | |
using assocPath? | |
James Forbes @JAForbes Feb 07 17:10 | |
Updated, they're inline now | |
dontwork @dontwork Feb 07 17:10 | |
ill bring ramda in, my reluctance for using stuff like this for a solution is that its introducing a new dependency and a hard to grasp concept | |
i could just pass lenspath a split string and it would get the correct proeprty and set it? | |
James Forbes @JAForbes Feb 07 17:11 | |
yep | |
and give you an entirely new state object | |
dontwork @dontwork Feb 07 17:11 | |
i mean that is pretty good isnt it | |
James Forbes @JAForbes Feb 07 17:11 | |
no Object.assign mess | |
dontwork @dontwork Feb 07 17:12 | |
ok brb ill put it in | |
James Forbes @JAForbes Feb 07 17:12 | |
and you can use over to transform a particular property too | |
dontwork @dontwork Feb 07 17:12 | |
lets not get ahead of ourselves | |
James Forbes @JAForbes Feb 07 17:12 | |
and view to access the value | |
ha, see I didn't want to wear out my welcome | |
did I mention lenses compose? :P | |
dontwork @dontwork Feb 07 17:12 | |
youre not wearing it out :P i just need to catch up :) | |
so i just need lensPath and set? | |
James Forbes @JAForbes Feb 07 17:13 | |
What's so cool about lenses is, because they return the root object, you can compose them again and again and string multiple transformations together without losing context, you always retain the complete object, no matter how focused (deep) our lens goes | |
yep | |
dontwork @dontwork Feb 07 17:14 | |
set returns a new object? | |
James Forbes @JAForbes Feb 07 17:14 | |
yep | |
dontwork @dontwork Feb 07 17:17 | |
so like this | |
case types.UPDATE_FORM_USER: | |
const { property, value } = action | |
var computed = lensPath(property.split()) | |
return Object.assign({}, | |
state, { | |
form: { | |
user: set( computed, value, state.form.user ) | |
} | |
} | |
) | |
James Forbes @JAForbes Feb 07 17:18 | |
case types.UPDATE_FORM_USER: | |
const { property, value } = action | |
const path = ['form', 'user'].concat( property.split('.')) | |
var computed = lensPath( path ) | |
return set( computed, value, state ) | |
yep, but if you include form in the path, you can skip the outer Object.assign | |
You might consider simply including the fullpath on your action, and then your entire reducer is set( lensPath( path ), value, state) | |
dontwork @dontwork Feb 07 17:19 | |
i mean i missed a '.' in my split function | |
and now im in heaven | |
;) | |
what do you mean provide the full path on my action | |
James Forbes @JAForbes Feb 07 17:20 | |
well see how I'm constructing the full path | |
You know what scratch that, now we can get into lens composition | |
dontwork @dontwork Feb 07 17:21 | |
yeah im on board now james, lets rob a bank, ill do anything | |
James Forbes @JAForbes Feb 07 17:21 | |
Lets say we have a lens that access form.user | |
:D | |
dontwork @dontwork Feb 07 17:21 | |
one sec | |
mind if we work from the code i have? | |
case types.UPDATE_FORM_USER: | |
const { property, value } = action | |
var computed = lensPath(property.split('.')) | |
return Object.assign({}, | |
state, { | |
form: { | |
user: set( computed, value, state.form.user ) | |
} | |
} | |
) | |
James Forbes @JAForbes Feb 07 17:21 | |
yeah sure | |
dontwork @dontwork Feb 07 17:21 | |
how can i make this the most compressed but readable | |
al settable properties are within user | |
James Forbes @JAForbes Feb 07 17:21 | |
So first lets create a lens for the user | |
const user = R.lensPath(['form', 'user']) | |
Then we create a lens for our path, in the reducer: | |
const { property, value } = action | |
var computed = lensPath(property.split('.')) | |
dontwork @dontwork Feb 07 17:22 | |
ok | |
James Forbes @JAForbes Feb 07 17:22 | |
Now we want to put the 2 together | |
const { property, value } = action | |
var computed = lensPath(property.split('.')) | |
var complete = compose( user, computed ) | |
dontwork @dontwork Feb 07 17:23 | |
oh no he didnt | |
James Forbes @JAForbes Feb 07 17:23 | |
now we've effectively got a lens: 'form.user.address.street' | |
dontwork @dontwork Feb 07 17:23 | |
so i need compose? | |
James Forbes @JAForbes Feb 07 17:24 | |
and all we need now is to plug the value in with the root state object | |
yep compose, is used to feed input and output from 1 function to the other, but it works seamlessly on lenses as well | |
dontwork @dontwork Feb 07 17:24 | |
ok | |
James Forbes @JAForbes Feb 07 17:24 | |
So the full thing now: | |
dontwork @dontwork Feb 07 17:24 | |
for this code i might skip the compose and just put them in the swecond lensPath | |
user will always be the top level user-settable object | |
James Forbes @JAForbes Feb 07 17:24 | |
// define in parent scope if you want | |
const user = R.lensPath(['form', 'user']) | |
// in the reducer | |
const { property, value } = action | |
var computed = lensPath(property.split('.')) | |
var complete = compose( user, computed ) | |
set( complete, value, state) | |
no object.assign at all! | |
dontwork @dontwork Feb 07 17:26 | |
i need to return set then do i ? | |
James Forbes @JAForbes Feb 07 17:26 | |
yep | |
Just for fun, let's make the lensPath( property.split ) a function | |
var dotLens = compose( lensPath, split('.')) | |
dotLens( property ) // creates the lens | |
dontwork @dontwork Feb 07 17:28 | |
ive ended up making this property | |
address,streetAddress | |
: | |
"1385 Eric Freewaydd" | |
this is what i did | |
case types.UPDATE_FORM_USER: | |
const { property, value } = action | |
var prop = lensPath(['form', 'user', property.split('.')]) | |
console.log(set(prop, value, state)) | |
return set(prop, value, state) | |
my bad | |
needed to spread the array | |
James Forbes @JAForbes Feb 07 17:32 | |
More fun: (this is just being silly, the above code was fine as is) | |
// a lens to access action.property, can create this ahead of time | |
var property = lensProp('property') | |
// a function that creates a lens for the split property string | |
var dotLens = compose( lensPath, split('.')) | |
// a function that create the lens from action.property and composes it with the user lens | |
// basically all the work we need to do, but completely lazily | |
// Action -> Lens | |
var actionLens = over( property , compose( user, dotLens ) ) | |
// in your reducer | |
// create the complete lens, and use it to set the value on the state object! | |
set( actionLens( action ), value, state ) | |
What's so cool is, we can create a function to access the path before we even know what the path is! | |
dontwork @dontwork Feb 07 17:34 | |
i mean i am pretty lost now | |
James Forbes @JAForbes Feb 07 17:34 | |
yeah I'm sorry | |
so did you code work though with the lens? | |
dontwork @dontwork Feb 07 17:34 | |
yep its working | |
James Forbes @JAForbes Feb 07 17:34 | |
you said to rob a bank | |
nice! | |
dontwork @dontwork Feb 07 17:36 | |
so how can i get rid of all these object assigns then | |
James Forbes @JAForbes Feb 07 17:37 | |
Just more lenses | |
You'll find you can push most of the lens generation out of the reducer, and just have them as top level functions, simply by leaving the state argument out | |
dontwork @dontwork Feb 07 17:38 | |
one sec | |
mind if i provide another example? | |
James Forbes @JAForbes Feb 07 17:38 | |
yeah go for it | |
dontwork @dontwork Feb 07 17:39 | |
case types.SET_FORM_USER: | |
return Object.assign({}, | |
state, { | |
form: { | |
validationErrors: state.form.validationErrors, | |
user: Object.assign({}, initialState.form.user, action.user) | |
} | |
} | |
) | |
i am assigning initialstate.form.user as well because it has all the property names the server might not send back | |
e.g emptys | |
ill need to enable treeshaking in webpack | |
my main.js went up about 700kb with ramda | |
James Forbes @JAForbes Feb 07 17:41 | |
you can require individual files via require('ramda/src/lensProp') for example | |
dontwork @dontwork Feb 07 17:41 | |
im es6ing | |
James Forbes @JAForbes Feb 07 17:43 | |
Well... ramda is es5, so unless your tooling does some kind of magic that would let you do import lensProp from 'ramda/src/lensProp' you might be out of luck? I thought require would still work though. | |
dontwork @dontwork Feb 07 17:44 | |
oh man | |
Barney Carroll @barneycarroll Feb 07 17:45 | |
That problem is solvable | |
There's a lib specifically for pruning your Ramda codebase | |
dontwork @dontwork Feb 07 17:45 | |
lol | |
what is it? | |
Barney Carroll @barneycarroll Feb 07 17:46 | |
As in, parse your code and modify the imports to only include the functions you used | |
Pretty sure it's a Web pack plugin | |
dontwork @dontwork Feb 07 17:46 | |
do you know the name? | |
Fred Daoud @foxdonut Feb 07 17:47 | |
@dontwork fwiw perhaps https://www.npmjs.com/package/object-path would be an alternative. | |
James Forbes @JAForbes Feb 07 17:47 | |
You'll probably save 700kb by removing all the Object.assigns | |
Fred Daoud @foxdonut Feb 07 17:48 | |
objectPath.set(object, "some.nested.path", value); | |
dontwork @dontwork Feb 07 17:49 | |
thanks @foxdonut | |
so @JAForbes how would the above look with some ramda-ising | |
Barney Carroll @barneycarroll Feb 07 17:52 | |
Here it is | |
https://github.com/dumconstantin/ramda-loader/ | |
dontwork @dontwork Feb 07 17:53 | |
interesting | |
i quite like having the imports at the top | |
Barney Carroll @barneycarroll Feb 07 17:53 | |
I'm sure you can still do the individual imports | |
AFAIK Webpack imports translating to a spec compliant ES6 import are the exception rather than the rule | |
*still = instead, ie without this loader | |
dontwork @dontwork Feb 07 17:56 | |
what do you mean barney? | |
James Forbes @JAForbes Feb 07 17:58 | |
For starters, can replace that whole thing with: | |
const user = lensPath(['form', 'user']) | |
// in your reducer: | |
case types.SET_FORM_USER: | |
set( user, merge( initialState.form.user, action.user ), state ) | |
cavemansspa @cavemansspa Feb 07 17:58 | |
@JAForbes -- on css vars analysis. | |
James Forbes @JAForbes Feb 07 17:58 | |
@cavemansspa Thanks | |
Barney Carroll @barneycarroll Feb 07 17:59 | |
@dontwork when @JAForbes says you should do import lensProp from 'ramda/src/lensProp' | |
I'd be very surprised if Webpack didn't handle that out of the box | |
dontwork @dontwork Feb 07 17:59 | |
oh! | |
James Forbes @JAForbes Feb 07 17:59 | |
That merge, can be moved out, because initial state never changes: | |
const user = lensPath(['form', 'user']) | |
const baseUser = merge( initialState.form.user ) | |
// in your reducer: | |
case types.SET_FORM_USER: | |
set( user, baseUser( action.user ), state ) | |
dontwork @dontwork Feb 07 18:01 | |
what is baseUser? | |
James Forbes @JAForbes Feb 07 18:02 | |
And we can optionally re-use our user lens to create baseUser | |
but your not saving much there... | |
const user = lensPath(['form', 'user']) | |
const baseUser = merge( view( user, initialState ) ) | |
its just a function that's going to accept some user properties later, and assign them on top of the initialState.form.user | |
dontwork @dontwork Feb 07 18:03 | |
ok | |
so what about this | |
case types.SET_EMPTY_FORM_USER: | |
return Object.assign({}, | |
state, | |
initialState | |
) | |
that can just be a set? | |
James Forbes @JAForbes Feb 07 18:03 | |
probably just merge( initialState, state), though I wonder when that case is used | |
is that to reset the form? | |
dontwork @dontwork Feb 07 18:04 | |
what case? | |
for creation | |
like a new user | |
James Forbes @JAForbes Feb 07 18:04 | |
well in that case, you can just return initialState can't you? | |
Barney Carroll @barneycarroll Feb 07 18:04 | |
Don't get how Redux re-popularised switch cases, I hate those things | |
James Forbes @JAForbes Feb 07 18:04 | |
and it will become state next time, by the nature of redux | |
dontwork @dontwork Feb 07 18:04 | |
yes good point | |
haha | |
you can do ifs barney ;) | |
oh | |
James Forbes @JAForbes Feb 07 18:05 | |
yeah I don't like switches either | |
dontwork @dontwork Feb 07 18:05 | |
@jaforbes there is a list of users | |
which initialState would delete | |
thats why | |
James Forbes @JAForbes Feb 07 18:05 | |
ohh | |
ok so return merge( initialState, state) | |
Barney Carroll @barneycarroll Feb 07 18:06 | |
It's so weird for this library to take the world by storm selling this paradigm shift of immutability, purity and functional composition… and then having switch cases as its core piece of author code | |
dontwork @dontwork Feb 07 18:06 | |
ok | |
James Forbes @JAForbes Feb 07 18:06 | |
and you can move that merge( initialState ) out if you want, and give it a name like initialize, up to you, I'd keep it inline | |
dontwork @dontwork Feb 07 18:06 | |
Ive just had too many issues managing state to worry about a switch statement | |
James Forbes @JAForbes Feb 07 18:07 | |
const initialize = merge( initialState ) | |
// in your reducer: | |
return initialize( state ) | |
Barney Carroll @barneycarroll Feb 07 18:07 | |
@dontwork chuck in some GOTOs for good measure | |
dontwork @dontwork Feb 07 18:07 | |
? | |
ok @jaforbes maybe a slightly more interesting one | |
case types.UPDATE_USER_SUCCESS: | |
return Object.assign({}, | |
state, { | |
list: [ | |
...state.list.filter(user => user.id !== action.user.id), | |
Object.assign({}, action.user) | |
] | |
} | |
) | |
James Forbes @JAForbes Feb 07 18:08 | |
Ok, this relates back to that video I linked earlier, but we'll put that to 1 side | |
You really just want to concat without duplicating right? | |
dontwork @dontwork Feb 07 18:09 | |
yes | |
James Forbes @JAForbes Feb 07 18:09 | |
and you want your new user as the last item | |
dontwork @dontwork Feb 07 18:09 | |
well | |
i dont care where it goes | |
i order in the components | |
at least at the moment | |
James Forbes @JAForbes Feb 07 18:17 | |
Ok so: uniqBy( prop('id') ) is a function that accepts a list and removes any duplicates. | |
We want to transform our state.list so we'll use over. And we want to concat our action.user into the list. | |
So: | |
const list = lensProp('list') | |
const insertUniq = x => | |
pipe( | |
concat([ x ]) | |
, uniqBy( prop('id') ) | |
) | |
// in your reducer | |
return over( list, insertUniq(action.user), state) | |
If we had a set, we could just concat, and the uniq stuff would be managed internally | |
dontwork @dontwork Feb 07 18:19 | |
hohoho | |
whats next he wonders | |
whats over? | |
James Forbes @JAForbes Feb 07 18:22 | |
over calls a function over the particular property your focused on with the lens, and then returns the complete object | |
set and view are just implemented in terms of over | |
Barney Carroll @barneycarroll Feb 07 18:22 | |
Basically Redux but in 4 bytes | |
dontwork @dontwork Feb 07 18:22 | |
+700kb | |
James Forbes @JAForbes Feb 07 18:22 | |
set is just (lens, value, state) => over( lens, () => value, state) | |
dontwork @dontwork Feb 07 18:23 | |
ok cool | |
so if merge is for merging 2 objects | |
what does set do | |
James Forbes @JAForbes Feb 07 18:23 | |
over is generally more useful when you get into composing stuff | |
e.g. say we have our list lens | |
set will replace an item at the property list, but with over, we could choose to append to the list, or prepend, or do someother transformation instead of a wholesale replacement | |
merge is just one of a million functions you could use in the context of over | |
Barney Carroll @barneycarroll Feb 07 18:24 | |
Not good enough @dontwork it's gotta be ternaries and bitwise operators or nothing | |
James Forbes @JAForbes Feb 07 18:26 | |
the transformation function over receives accepts 2 args, a value, and the existing state/context. | |
So you can plug in a lot of ramda functions with that recipie. E.g. assocPath. Which is how lensPath works behind the scenes | |
dontwork @dontwork Feb 07 18:28 | |
i will watch the ramda course on pluralsight tonight | |
James Forbes @JAForbes Feb 07 18:28 | |
I didn't know about it! | |
ok cool | |
dontwork @dontwork Feb 07 18:28 | |
https://app.pluralsight.com/library/courses/javascript-ramda-functional/table-of-contents | |
one more? | |
return Object.assign({}, | |
state, { | |
list: [ | |
...state.list, | |
Object.assign({}, action.user) | |
] | |
} | |
) | |
sorry if it seems like im taking the piss :D | |
James Forbes @JAForbes Feb 07 18:29 | |
so you can skip Object.assign({}, action.user) completely | |
And now we're just left with return over( list, concat([action.user], state ) | |
Barney Carroll @barneycarroll Feb 07 18:30 | |
So awesome | |
dontwork @dontwork Feb 07 18:30 | |
isnt it | |
its addictive | |
so with this for instance | |
Barney Carroll @barneycarroll Feb 07 18:31 | |
This is really good from a lurker's perspective | |
dontwork @dontwork Feb 07 18:31 | |
return Object.assign({}, | |
state, { | |
list: action.users | |
} | |
) | |
you see that and then what do you think @JAForbes | |
James Forbes @JAForbes Feb 07 18:31 | |
ah that's good to know, I felt it'd be grating | |
dontwork @dontwork Feb 07 18:31 | |
it should use a lens? | |
James Forbes @JAForbes Feb 07 18:31 | |
its just set( list, actions.users, state) | |
dontwork @dontwork Feb 07 18:31 | |
right | |
so list is a lensProp, is that different to a lensPath | |
obviously it is | |
James Forbes @JAForbes Feb 07 18:32 | |
well... | |
ok ITS TIME TO REVEAL THE MAN BEHIND THE CURTAIN | |
lensPath is just a series of composed lensProp's | |
dontwork @dontwork Feb 07 18:32 | |
lol | |
so that code would work with lensPath instead of prop? | |
James Forbes @JAForbes Feb 07 18:33 | |
const abc = lensPath(['a', 'b', 'c']) | |
const abc = compose( lensProp('a'), lensProp('b'), lensProp('c') ) | |
its all lenses, its all built on top of the same low level function | |
which is just lens( getter, setter) | |
dontwork @dontwork Feb 07 18:33 | |
how did you get into all this voodoo? | |
James Forbes @JAForbes Feb 07 18:34 | |
I'm as surprised as anyone | |
I think just lurking in the ramda chat? | |
Here's how to define lensProp using the base lens constructor: | |
const lensProp = x => lens( prop(x), assoc(x) ) | |
spacejack @spacejack Feb 07 18:35 | |
lol! @JAForbes | |
James Forbes @JAForbes Feb 07 18:35 | |
@spacejack | |
Barney Carroll @barneycarroll Feb 07 18:35 | |
Stop decomposing! | |
dontwork @dontwork Feb 07 18:35 | |
i cant cope | |
James Forbes @JAForbes Feb 07 18:36 | |
technically lensPath is defined using const lensPath = x => lens( path( x ), assocPath( x ) ) but it could be defined via compose | |
With ramda it often matters more that something could be defined mathematically, when the underlying code might actually use a while loop for performance etc | |
It matters that lensPath obeys the lens laws | |
its what makes it composable / predictable | |
And then .... | |
Fred Daoud @foxdonut Feb 07 18:37 | |
Brian Lonsdorf (aka Dr. Boolean) is the voodoo master we are his disciples | |
James Forbes @JAForbes Feb 07 18:37 | |
yep | |
Gega Nizharadze @Bravilogy Feb 07 18:37 | |
+1 to that | |
Barney Carroll @barneycarroll Feb 07 18:38 | |
Erm newsflash guys | |
Voodoo masters don't have disciples | |
The correct term is zombie | |
Fred Daoud @foxdonut Feb 07 18:39 | |
that's the beauty of it.. we don't know that we are zombies. all part of the genius of the voodoo master. | |
dontwork @dontwork Feb 07 18:41 | |
can anyone tell me why this page has the navbar twice? https://bspqpzjkmz.localtunnel.me/users/create | |
James Forbes @JAForbes Feb 07 18:42 | |
oh @barneycarroll I only just got: decomposing -> zombies | |
Barney Carroll @barneycarroll Feb 07 18:42 | |
Whereas normal people are disgusted by the notion that they are ultimately just a recursive composition of basic mechanical entities, you guys are proud of it. Makes me sick. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment