Skip to content

Instantly share code, notes, and snippets.

@orbitbot
Last active March 15, 2017 09:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save orbitbot/292059bbe30400234ba4dc3ebb92ec51 to your computer and use it in GitHub Desktop.
Save orbitbot/292059bbe30400234ba4dc3ebb92ec51 to your computer and use it in GitHub Desktop.
the Mithril dialogues - insightful exchanges in the chatroom
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