reagent-project / reagent

A minimalistic ClojureScript interface to React.js
http://reagent-project.github.io/
MIT License
4.75k stars 414 forks source link

how to keep a parent from updating ? #182

Closed jdkealy closed 9 years ago

jdkealy commented 9 years ago

Hi,

I'm probably doing something wrong here, but I'll perhaps you can help me.

I've got a global app state, a page, a list of components, and an optional component that shows when one of the components in the list is "on". When that component mounts it does a fetch call affecting the global app state. I guess, somewhat predictably, the page re-renders and the state defined in the component loop is re-initialized. I'd like to figure out a way to keep the page from updating and preserve my component state.

In psuedo-code:

(defn page [] :component-did-mount "FETCH ALL PHOTOS" :reagent-render

(defn photo-component [item state] [:div {:on-click "make the state show the add the 'add to lightbox diologue'")} (when "adding-to-lightbox" "show the add to lightbox component" ) )

(defn add-to-lightbox-compnent [] :did-mount (if i don't have any lightboxes, fetch them) :render (loop over lightboxes to choose one )

My problem is that when i fetch the lightboxes in the last step "page" gets reinitialized, which then reinitializes the state of each photo. I could fetch all the lightboxes on the page did mount, but i'd rather not do that.

regarding my global app-state. It looks like this

(atom {:photos [ 1, 2 ,3 ], :lightboxes [x y z ]})

jdkealy commented 9 years ago

I guess my question in this case is basically... How can I bind the page do just the :photos part of my app-state ?

GetContented commented 9 years ago

You can use a cursor.

Assuming you have imported reagent.core :as r, and the atom you refer to is an r/atom, not a clojure.core/atom, you can write this: (r/cursor atom-name [:photos]) and that will act like you've just got a vec with [1,2,3] in it. This acts like a lens, or a view that you can also write to.

If this solves your issue, please close this.

jdkealy commented 9 years ago

I'm confused as to where I would be setting that value.

I hage Page, PhotoComponent, LightboxSelector

Page gets called by secretary. Page has a did-mount func that calls get-all-photos, calls a "markup" function, which loops over photos and creates the photo component.

If I understand your answer correctly, any component that receives an r/cursor as an argument will automagically not update if something outside the cursor is updated ?

GetContented commented 9 years ago

@jdkealy I don't want to be presumpuous, or cause offense, but it sounds a bit like you might not fully understand how React works.

When you write a component, you're defining a render function for React to use to render your component. You should not feel like you are telling the framework when or how to do the rendering. That is, don't think imperatively, think declaratively. React will mount your component, and call the did-mount function. After that, it may call your render function many many times. If you have a dereffed r/cursor or r/atom within the body of that function, Reagent will make React update the component (ie call your render function again). If any of the arguments to the render function change, React will update the component (ie call the render function again). Note that passing an un-dereffed r/atom or r/cursor as arguments and then changing the data they wrap will not cause re-rendering because you actually haven't changed the argument, you've changed what the argument refers to. Hope that's clear.

So, you are not in control of when your render functions are called. This is very important, because if you put a piece of code that changes data inside your render function, it will cause havoc.

You can use the did-mount callback to do any data-gathering, or initial set up you need (like local r/atoms, etc.).

You haven't given enough information for me to comment on your specifics, but this general answer should hopefully be enough information for you to be able to answer your questions yourself.

jdkealy commented 9 years ago

Ah thanks for the answer. No offense taken :) I'm just used to the whole passing-props down paradigm from react itself and om. Reagent makes it a little less clear what's happening. I have been trying to follow a flux architecture making "stores" files and accessing my data through them. e.g.

photos defn get-photos[]
get @global/app-state :photos))

I'll try to rework my logic to create a cursor instead. Thanks!

GetContented commented 9 years ago

@jdkealy depending on the size of the app you're creating, I'd recommend looking at re-frame https://github.com/Day8/re-frame

and also if you want to understand reagent components, I actually think it's conceptually simpler than Om. This is a good explanation: https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components

Also, if you haven't watched it, David Nolen's Om/Next talk describes some of the problems with the existing Om architecture. https://www.youtube.com/watch?v=ByNs9TG30E8

jdkealy commented 9 years ago

I read that bit about Components but it does address props or cursors. Looking forward to that DN vid. I built an app with it and loved it, but glad I made the move to reagent.

GetContented commented 9 years ago

The trouble with building large react apps is you often need a way for components to register their interest in data and its changes.

When that data can be got from its parent component, all is well: (untested example)

(defonce person-store (r/atom [{:name "David" :children-names ["Henrietta" "Juliet" "Tracy"]}]))

(defn person-name
  [name]
  [:label name])

(defn parent [person]
  [:div 
    [person-name (:name person)]
    [:div {:class-name "children"}
      (for [child-name (:children-names person)]
        ^{:key (:child-name)}
        [person-name child-name])]])

It's all peachy!

However, what if the person-name component needs to get some external piece of data that the parent can't pass down to it, because it doesn't "know" it? Things start getting a bit flaky. Do you pass it down through the parent, from outside the parent? Maybe, but that can cause some performance issues or unnecessary rendering, potentially, and is pretty messy (your components should be separable from their context, otherwise you can't re-use them, even within your app). Or, do you perhaps just directly use a r/atom reference or cursor within the person-name component? That architecture quickly leads to a mess as you can't follow what's using what, and things are heavily coupled to their context.

Re-frame, and other subscription-based architectures (like flux and om/next) have a story for what to do when this problem manifests. Essentially you create a subscription into the data, and when the data changes, it lets all subscribers re-render (in Reagent's case, using a Reaction, which is the abstraction mechanism that allows r/cursor and r/atom objects to get React to re-render components when their underlying data changes). This is a lot cleaner because the component doesn't need to know the details of how it gets the data, all it knows is that it gets handed some data, in the form it needs it to be in.

If you're building a simpler application, then this isn't that important and you don't need this amount of architecture.

Re-frame uses the idea of processed (ie computed) signals over time - of data flowing through the application and components reacting to it. This is by far the best idea, because a component should not know the details of how to get its data, it should only need to know how to express to the surrounding application that it has a requirement of some data, and the application should supply it to it, and also update it when that data changes, but not otherwise.

mike-thompson-day8 commented 9 years ago

@jdkealy there's a school of thought (repressive cult?) which holds (belligerently insists?) that read/write cursors are bad for architecture (the spawn of Satan?).

Coming from OM, you may be unaware of the moral risks that you have been running. :-) If you are interested in changing your corrupt ways, and renouncing cursor use, then re-frame (or Hoplon or zelkova or Elm) may be interesting to you. They will lead you in the noble direction of FRP.

Good luck :-)

jdkealy commented 9 years ago

Hey by the way, in spite of the daunting last message, the use of cursors kept my page from refreshing, so thanks for that! Basically had to avoid accessing ANY root level attributes inside my page and it does not get re-rendered. cool

I would look into some other framework, but I have a deploy date in 7 days :)

When I have some fresh air and sanity, I will take a look. That's two recommendations. Must mean something.

GetContented commented 9 years ago

@jdkealy yeah, you gotta get the paid work done first!

I just finished re-implementing our app (content & design editor for all webpages built for our business www.getcontented.com.au) using a simple subscription-based data-flow-signal-computation and caching graph system, and I gotta tell you, it's absolutely amazing. The way it's supposed to work. Everything's fast. Nothing re-renders unless data on the backend changes, and it only (re-)computes what it needs to compute, caching all the data and computation along the way. I often have thousands of components on the page at once, and it doesn't break a sweat now.

jdkealy commented 9 years ago

Looks awesome: This is my demo site: http://ec2-52-22-28-98.compute-1.amazonaws.com/events/12790

You can see the lightbulb under each photo where you can "add to lightbox" no more flicker since I implemented cursors. This site will be replacing http://bfa.com/

jdkealy commented 9 years ago

and of course... i just found a # of bugs

On Fri, Aug 28, 2015 at 12:40 AM, www.getcontented.com.au < notifications@github.com> wrote:

@jdkealy https://github.com/jdkealy yeah, you gotta get the paid work done first!

I just finished re-implementing our app (content & design editor for all webpages built for our business www.getcontented.com.au) using a simple subscription-based data-flow-signal-computation and caching graph system, and I gotta tell you, it's absolutely amazing. The way it's supposed to work. Everything's fast. Nothing re-renders unless data on the backend changes, and it only (re-)computes what it needs to compute, caching all the data and computation along the way. I often have thousands of components on the page at once, and it doesn't break a sweat now.

— Reply to this email directly or view it on GitHub https://github.com/reagent-project/reagent/issues/182#issuecomment-135630946 .

Sincerely -John-