+ All Categories
Home > Documents > Rails Meets React Sample

Rails Meets React Sample

Date post: 17-Aug-2015
Category:
Upload: cristian-rodriguez
View: 222 times
Download: 3 times
Share this document with a friend
Description:
Rails Meets React Sample
24
Transcript

Anatomy of the React componentLets see a really simple example of a React component:Greeter=React.createClassrender:->React.DOM.spanclassName:'greeter-message'"Hello,#{@state.name}!"To master React, you need to know the components structure. Basically, components in React aremade of three basic parts: state - React components maintain their dynamic part of data separated from the static datafor performance reasons. Manipulating components is done by changing the state doing soforces React to re-render a component. properties - React components get properties in the moment they are instantiated. You canuse themto create the initial state and set values that need to be accessible and are constantthrough the whole lifecycle of a component. render method - The render method should always return a React component. It has accessto properties and state and they (and only they) decide what will be the outcome of the rendermethod. The render method should only generate HTML and assign key/refs/event handlersto it. If you generate it using only state and properties, you achieve what React developers callside-effect free render method. You should aim to do so every time it makes React faster,makes your development and testing easier.Thats it. These three parts need to be present in all React components you create. Of course it is notall - React components come with some additional features that will be discussed later.StateIn the world of dynamic user interfaces the user interface is stateless statement (which templatelibraries like Handlebars or Mustache repeat like a mantra) is hard to defend the more complexUI, the harder it may be to defend it. React developers know it thats why they made componentsstate one of the most important concepts in the whole library.Anatomy of the React component 76Even the simplest input mechanism in browsers has state.Changing state should be the only way to re-render your component, resulting in a visible change state is available during rendering of the component and in lifecycle methods of it (discussed later).You may think of it as dynamic data of a component with an additional side-effect: Changingstate forces re-rendering of component.By default, when instantiating a component, the state is empty it is a null value. You can changeit using thesetState method which every component has. Note that you cannot manipulatethe state of unmounted components. All components start as unmounted you must explicitlycall the React.renderComponent prototype method to make it mounted which allows you to usesetState and other components instance methods.State is accessible in most component methods under @state instance variable. In theGreetercomponent you directly access the name field of the @state inside render method.Common mistakesConsider this small snippet of code.Greeter=React.createClassrender:->React.DOM.spanclassName:'greeter-message'"Hello,#{@state.name}!"greeter=React.createFactory(Greeter)element=greeter()element.setState(name:'John')#?In this case the setState invocation results in an error since elements do not have state. Only therendered components have state. Calling setState in such situation will result in an error.Here is another, more tricky example.Anatomy of the React component 77Greeter=React.createClassrender:->React.DOM.spanclassName:'greeter-message'"Hello,#{@state.name}!"greeter=React.createFactory(Greeter)component=React.render(greeter(),document.getElementById("greeter-placeholder"))component.setState(name:'myfirstReactcomponent!')#?React.render will throw thenull is not an object exception. Why is that? BecauseReact.render calls the render method from the component that is being mounted. Rememberthat @state by default is null. So calling @state.name inside the render method raises an errorin such situation.Ifrender finished successfully itd be absolutely fine to ask for the state change. To fix this code,you need to know how to provide the initial state to our components. Lets see how to do this.Setting initial stateSince state in components is null by default, itd be hard to make something meaningful with Reactif there was no way to have control the initial state. In fact, without such control you wouldnt be ableto create any dynamic components at all. Since there will be only properties available and propertiesare static, they shouldnt change. As expected, React components provide you such control. You canoverride the getInitialState method in our component to create the initial state that will be usedbefore any setState happens.Using getInitialState allows you to use Greeter componentGreeter=React.createClassgetInitialState:->name:'World'render:->React.DOM.spanclassName:'greeter-message'"Hello,#{@state.name}!"After this change to the Greeter component our last example is going to work as expected:Anatomy of the React component 78greeter=React.createFactory(Greeter)component=React.render(greeter(),document.getElementById("greeter-placeholder"))#Hello,World!willbe#placedinside#greeter-placeholderelement,overridingpreviouscontent.component.setState(name:'myfirstReactcomponent!')#Componentisre-rendered,soitresultsin"Hello,myfirstReactcomponent!"#asopposedto"Hello,World!"youhadbefore.The important part about getInitialState is that you can use properties to instantiate the initialstate. That means you can easily make defaults overridden.Maintaining good stateWhat data should be stored inside the state? There are a few rules of thumb:1. Data which wed like to change in order to force re-render the component. Thats the ideabehind state after all!2. You should rather store raw data which allows you to compute all needed further datarepresentations in the render method. State is not a place to duplicate data since its our mainsource of truth you want to keep it as consistent as possible. The same applies to properties. Ifyou are thinking about which of the two things you should put into the state A or B and Bcan be computed from A, you should definitely store A in the state and compute B in rendermethod.3. No components in state. Components should be created in therender method based onthe current state and properties provided. Storing components in state is discouraged andconsidered as an anti-pattern.4. Things that you need to dispose when component dies. Especially important in integrationwith external libraries.If your component is keeping reference to 3rd party library object (for example carousel, popup,graph, autocomplete) you need to call remove() (or some kind of equivalent of it) on it when theReact component is unmounted (removed from a page). You can define your clean-up algorithm,overriding the defaultcomponentWillUnmount method. This method will be called just beforeunmounting your component.PropertiesYou have seen that you can go quite deep even without knowing what properties are. But propertiesare a very helpful part of all React components you are going to create. The basic idea is that theyAnatomy of the React component 79are data which have to be unchanged during the whole lifecycle of the component whether toinstantiate a default state or to reference this data while defining components behaviour and therender outcome. You may think about them as constants in your component you may referencethem during rendering or setting components behaviour, but you shouldnt change them.React developers often say that properties are immutable. That means that after construction ofthe component they stay the unchanged. Forever. If its a complex object you can of course invokemethods on it but it is only because its hard to ensure for React that something changed. Reactassumes that changes to properties never happen. Changing properties, even by invoking methodson it is considered an anti-pattern.You pass properties during construction of an element. Basically, every factory is a function withthe following signature:factory(properties,children)Properties are accessible even in elements via the @props field. As I mentioned before, you can alsouse them in getInitialState to parametrize default state creation:Example: Passing a parameter to instantiate default stateGreeter=React.createClassgetInitialState:->name:@props.initialName||"World"render:->React.DOM.spanclassName:'greeter-message'"Hello,#{@state.name}!"rootNode =document.getElementById("greeter-box")greeter=React.createFactory(Greeter)component=React.render(greeter(initialName:"Andy"),rootNode)#Hello,Andy!component.setState(name:'Marcin')#Hello,Marcin!React.unmountComponentAtNode(rootNode)component=React.render(greeter(),rootNode)#Hello,World!This use case is a quite common usage of properties. Another common usage is to pass dependenciesto components:Anatomy of the React component 80Example: Metrics tracking tool needs to be notified about user decision of reading full content of blogpost.DOM=React.DOMBlogPost=React.createClassgetInitialState:->fullContentVisible:falserender:->DOM.divclassName:'blog-content'DOM.h1key:'blog-title'className:'title'@props.blogpost.titleDOM.pkey:'blog-lead'className:'lead'@props.blogpost.leadDOM.akey:'blog-more'href:"#!/more"onClick:@continueReadingClicked"Continuereading->"[email protected]:'blog-full-content'@props.blogpost.fullContentcontinueReadingClicked:(event)->[email protected]@props.metricsAdapter.track('full-content-hit')@setStatefullContentVisible:trueblogPost=React.createFactory(BlogPost)post=blogPost(metricsAdapter:@googleAnalyticsAdapter)componentInstance=React.renderComponent(post,document.getElementById('blog'))Properties allow you to store all references that are important for our component, but do not changeover time.You can use properties alone to create React components for your static views. Such components arecalled stateless components and should be used as often as possible.Anatomy of the React component 81Stateless componentsIn our previous examples you were using components from the React library which are providedto create basic HTML tags you know and use in static pages. If you look at them closely you maynotice that they are stateless. You pass only properties to elements of their type and they maintainno state. State is an inherent part of more dynamic parts of your code. However it is advisable tokeep display data only components stateless. It makes code more understandable and testable andits generally a good practice.Displaying person information does not need state at allDOM=React.DOMPersonInformation=React.createClassperson:->@props.personrender:->DOM.divclassName:'person-info'DOM.headerkey:'info-header'DOM.imgkey:'avatar'className:'person-avatar'src:@person().avatarUrlalt:''DOM.h2key:'full-name'className:'person-name'@person().fullNameThe rule of thumb here - when possible, create stateless components.Setting default propertiesIt is often helpful to create some kind of default state while it is not necessary, it may reduceconditionals in your code, making it easier to read and maintain. Like with getInitialState, youhave also a similar method for properties and it is called getDefaultProps.Anatomy of the React component 82Unnecessary or statement can be avoided here with setting default state.Greeter=React.createClassgetInitialState:->name:@props.initialName||"World"render:->#...Fixed version of the code above.Greeter=React.createClassgetInitialState:->@props.initialNamegetDefaultProps:->initialName:'World'render:->#...You may also find it useful to use this method to improve declarativeness of your render methodwith this neat trick:Avoiding conditionals in render method would make it shorter and easier to comprehend.OnOffCheckboxWithLabel=React.createClassgetDefaultProps:->onLabel:"On"offLabel:"Off"id:'toggle'initiallyToggled:falsegetInitialState:->toggled:@props.initiallyToggledtoggle:->@setStatetoggled:[email protected]:->React.DOM.divclassName:'on-off-checkbox'React.DOM.labelkey:'label'Anatomy of the React component 83htmlFor:@[email protected]@[email protected]:'checkbox'type:'checkbox'id:@props.idchecked:@state.toggledonChange:@toggleThis neat trick allows you to avoid conditional in label.OnOffCheckboxWithLabel=React.createClassgetDefaultProps:->labels:true:"On"false:"Off"id:'toggle'initiallyToggled:false#...render:->#...React.DOM.labelkey:'label'htmlFor:@[email protected][@state.toggled]#...Properties defined in getDefaultProps are overridden by properties passed to an element.That means setting messages or strings in default properties makes our components powerfullyconfigurable out-of-the-box. It greatly improves reusability.Anatomy of the React component 84You can use our on/off toggle with more sophisticated labels for free with default properties approach.coolFeatureToggle=OnOffCheckboxWithLabellabels:true:"Coolfeatureenabled"false:"Coolfeaturedisabled"id:"cool-feature-toggle"As you can see, relying on default properties can have many advantages for your component. If youdo not want to have configurable messages, you can create a method inside the component and putthe labels object there. I like to have sensible defaults and to provide ability to change it to matchmy future needs.Component childrenLets look at the React factory arguments once again:factory(properties,children)While you already have detailed knowledge about properties, the children argument remains amystery until now. As you may know, HTML of your web page forms a tree structure you haveHTML tags nested in another tags, which nest another tags and so on. React components can haveexactly the same structure and thats what the children attribute is for. You can pass childrencomponents as a second argument of your component its up to you what youll do with it.Typically you pass an array of components or a single component there. Such children componentsare available in a special property called children:Using children in components from React.DOM namespace.React.DOM.divclassName:'on-off-checkbox'React.DOM.labelkey:'label'htmlFor:@[email protected]@[email protected]:'checkbox'type:'checkbox'id:@props.idAnatomy of the React component 85checked:@state.toggledonChange:@toggle#equals:React.DOM.div({className:'on-off-checkbox'},[React.DOM.label(...),React.DOM.input(...)])You can access children using a special children field inside properties.WhoIsInvited=React.createClassrender:->React.DOM.divclassName:'who-is-invited-box'React.DOM.h3key:'header'className:'who-is-invited-header'"You'veinvitedthispeopletotonight'spajamaparty:"React.DOM.ulkey:'invited-list'className:'who-is-invited-list'[email protected]:"person-#{personInformationComponent.person().id}"className:'who-is-invited-list-item'personInformationComponentwhoIsInvited=React.createFactory(WhoIsInvited)invitations=whoIsInvited({},[KidInformation(...),AdultInformation(...),AdultInformati\on(...)])#@propschildrenwillcontain#[KidInformation(...),AdultInformation(...),AdultInformation(...)]array.This feature allows you to easily create some kind of wrapper components (I call it open components)like a list above. Its quite common to have these kind of user interface elements many well-established generic UI solutions can be implemented this way. For example accordion boxes,drop-down menus, modals all of these generic solutions you know and use can follow this pattern,since they are generally containers to our content.Anatomy of the React component 86Accordion boxes are great candidates to be implemented as open components in React.Properties and state are a way of React component to handle data which can be a result of user actionsor events from the external world (messaging mechanism like Pusher, AJAX calls, websockets, etc.).While state is all about dynamism of components (and thats why you have switched fromRails views to frontend applications after all), properties allow you to provide data for morestatic purposes in our component.render methodLast, but not least, all React components must have therender method. This method alwaysreturns a React component. Since you compose your component from smaller components(with components provided in React.DOM namespace as leafs of your components tree) it is anunderstandable constraint here.Since React 0.12, it can return false or null as well. In such case React will render an invisible tag.What is important, you should never call therender method explicitly. This method is calledautomatically by React in these two situations: If the state changes. It also calls componentDidUpdate method. If you mount component using React.render. It also calls the componentDidMount method.The lifecycle methods such as the mentioned componentDidMount and componentDidUpdate willbe discussed later in the book.An important assumption that React makes about your render method is that it is idempotent -that means, calling it multiple times with the same properties and state will always result in thesame outcome.React components style (especially the built-in React.DOM components) resembles HTML buildersa bit. It might look like Haml to you and it is basically the same concept. You create componentswhich are transformed into your HTML.Anatomy of the React component 87What you achieve with state and properties is dynamic nature of this HTML - you can click onsomething to make an action, change the rendering of this HTML and so on. It looks really familiarto what you may find in Haml view files:Example of Haml view%section.container%h1=post.title%h2=post.subtitle.content=post.contentReact counterpartDOM=React.DOM#...DOM.section(className:'container',DOM.h1({},@state.post.title),DOM.h2({},@state.post.subtitle),DOM.div(className:'content',@state.post.content)What you should do in the render method is: Computing the auxiliary data you may need in your user interface from state and properties.You should not cache this result in those places. Id recommend to only call computationmethods and never inline this logic inside render.Example of precomputing values in render method.PersonGreeter=React.createClassgetInitialState:->person:newPerson('Bruce','Wayne')personFullName:(person)->[person.firstName,person.lastName].join("")render:->fullName=@personFullName(@state.person)#Computingfullnameofperson@greeterBox(@greeterSpan("Hello,#{fullName}!"))greeterBox:(children)->Anatomy of the React component 88React.DOM.divclassName:'greeter-box'childrengreeterSpan:(children)->React.DOM.spanclassName:'greeter-text'children Creating components based on properties and state. Since you should never store componentsin state or properties, the React way is to construct components inside the render method.Creating components based on properties and state.BooksListItem=React.createClassrender:->React.DOM.li({},@props.book.name)booksListItem=React.createFactory(BooksListItem)BooksList=React.createClassrender:->React.DOM.ul({className:'book-list'},[[email protected]({book:book})#Aboveyoucreatecomponentfrombooksinourproperties.]) Define the HTML structure of a user interface part. That means you can compose the returnvalue from React.DOM components directly or with higher level components. Eventually it istransformed into HTML.Do not try to memoize data in the render method. Especially components. React is managingthe components lifecycle using its internal algorithm. Trying to re-use component by hand is askingfor trouble, since it is easy to violate React rules for re-rendering here.JSXJSX is a JavaScript syntax extension created by React developers to make declaration of componentssimilar to HTML. It fixes the pain of creating components in a strict and unwieldy JavaScript syntax,since React is used by their creators in a JavaScript environment. In my personal opinion, sinceRails comes with CoffeeScript, you dont need it. CoffeeScript syntax is flexible enough to declareReact components in a neat way. Building the render method without JSX in CoffeeScript makesit more similar to Haml than to ERB. Even if you decide you dont need JSX, you must still knowAnatomy of the React component 89how to desugar JSX to a real code. It is because most examples youll find on the Internet for Reactare JSX-based. Without this skill you can take little value from even valuable sources of informationabout React.Using JSX in CoffeeScriptIf you disagree with me and want to use JSX in a CoffeeScript environment, you need to rememberabout a few things: You have to rename the file where you store a component with JSXfromsomething.js.coffeeto something.js.jsx.coffee. Note the jsx after js extension part. You must start and finish your component with a backtick (\) character.Fallbacking to JSX is often a good choice if you have your templates written using ERB - you canjust copy and paste HTML inside the file and change all Ruby-managed places in a view to datataken from properties and/or methods you define inside a component.Example component using JSX in CoffeeScript.#File:post_counter.js.jsx.coffeePostCounter=React.createClassrender:->`Thismonththereare{this.props.postCount}publishedpostsalready.Newpost`Using CoffeeScript flexible syntax to implement render methodYou can use CoffeeScript to build yourrender method. There are some tips about CoffeeScriptsyntax that you can use: You can shorten the React.DOM to just DOM or something even shorter, like R at the top of thefile. It makes script lines a little shorter and as an effect easier to read.Anatomy of the React component 90Shortening React.DOM to something shorter.R=React.DOMPostCounter=React.createClassrender:->R.div(className:'post-counter',["Thismonththereare#{@props.postCount}publishedpostsalready.",R.a({key:'cta',href:'/posts/new'},"Newpost")]) You can omit the parentheses and rely on a CoffeeScripts object syntax to list properties in amore readable way.Using coffeescript object syntaxR=React.DOMPostCounter=React.createClassdisplayName:'PostCounter'render:->R.divclassName:'post-counter'["Thismonththereare#{@props.postCount}publishedpostsalready.",R.akey:'cta'href:'/posts/new'"Newpost"] CoffeeScript treats multiple statements (separated by newline) in an argument list as collectionmade of this statements. That means you can omit [ and ] also, making it a very Haml-looking:Omitting explicit collection makes this render body more Haml-like.R=React.DOMPostCounter=React.createClassdisplayName:'PostCounter'render:->R.divclassName:'post-counter'"Thismonththereare#{@props.postCount}publishedpostsalready."R.aAnatomy of the React component 91key:'cta'href:'/posts/new'"Newpost"Note that if you dont pass any properties youll have a syntax error trying to pass childrenthis way.There is a problem if you dont pass properties to a componentR=React.DOMPostCounter=React.createClassdisplayName:'PostCounter'render:->R.div"Thismonththereare#{@props.postCount}publishedpostsalready."R.akey:'cta'href:'/posts/new'"Newpost"#SYNTAXERROR!The most elegant workaround Ive found is to restore square brackets in a collection and putit in a children property explicitly (notice you still dont need commas between collectionelements):Passing to children explicitly and restoring square brackets is the most elegant way Ive found to solvethis issue.R=React.DOMPostCounter=React.createClassdisplayName:'PostCounter'render:->R.div#Passedchildrencomponentsto`children`propertyexplicitly.children:["Thismonththereare#{@props.postCount}publishedpostsalready."R.akey:'cta'href:'/posts/new'"Newpost"] Create many small methods to hide logical parts of your component and provide a sensiblenaming. It is a great way to improve readability of your React code.Anatomy of the React component 92R=React.DOMPostCounter=React.createClassdisplayName:'PostCounter'render:->@postCounterBox[@postCountThisMonthMessage()@callToActionLink()]postCounterBox:(children)->R.divchildren:childrenpostCountThisMonthMessage:->"Thismonththereare#{@props.postCount}publishedpostsalready."callToActionLink:->R.akey:'cta'href:'/posts/new'"Newpost"These practices are working well in my projects, especially if I work on bigger components.Desugaring JSXDesugaring JSX is quite easy. Consider the following JSX example (taken from the official docu-mentation):JSX example to desugar

For each component, JSX is composed this way:Anatomy of the React component 93JSX is composed like this.[children]or:#childrensettonullThere is a special notation inside curly brackets for a runtime evaluation.That means the previous JSX desugars to this:Desugared JSX exampleprofilePic=React.createFactory(ProfilePic)profileLink=React.createFactory(ProfileLink)React.DOM.div({},[profilePic(key:'pic',username:@props.username)profileLink(key:'link',username:@props.username)])Desugared JSX example (inlined)React.DOM.div({},[React.createElement(ProfilePic,key:'pic',username:@props.username)React.createElement(ProfileLink,key:'link',username:@props.username)])You can also use JSX compiler from the official site. Beware: it produces JavaScript as the output soyou need to transform it to CoffeeScript then. Thats why I recommend to learn desugaring manually.If you understand it well itll be a no-brainer for you.SummaryIn this chapter you learned about the three main building blocks of React components state,properties and render method. This alone is a very complete knowledge that you can use straightaway. Since you know how to create a component, feed it with data and manipulate it, you canmake fairly complex components even now. What you lack is mastering how to handle actions touser and the external world. The next chapter is all about it.Bonus: Syntax comparison of React ComponentsAs a bonus we present a comparison of more complex components syntax. You can choose whatreally suits you.HTMLAnatomy of the React component 94

ProductnamePriceTax

Product1$12.00$1.00

Product2Unavailable

JSvarAvailableProductComponent=React.createClass({render:function(){returnReact.DOM.tr(null,React.DOM.td({key:'name'},this.props.name),React.DOM.td({key:'price'},this.props.price),React.DOM.td({key:'tax'},this.props.tax));}});varavailableProductComponent=React.createFactory(AvailableProductComponent);varUnavailableProductComponent=React.createClass({render:function(){returnReact.DOM.tr(null,React.DOM.td({key:'name'},this.props.name),React.DOM.td({key:'unavailable',colSpan:2},"Unavailable"));}});varunavailableProductComponent=React.createFactory(UnavailableProductComponent);varBasketComponent=React.createClass({Anatomy of the React component 95render:function(){returnReact.DOM.div({className:"basket"},React.DOM.table({key:'table'},React.DOM.thead({key:'head'},this.props.headers.map(function(header){returnReact.DOM.th({key:"th-"+head\er},header)})),React.DOM.tbody({key:'body'},availableProductComponent({key:'available-product',name:"Product1",price:\"$12.00",tax:"$1.00"}),unavailableProductComponent({key:'unavailable-product',name:"Product2"}))));}});varbasketComponent=React.createFactory(BasketComponent);React.render(basketComponent({headers:["Productname","Price","Tax"]}),$('div')[0]);JS + JSXvarAvailableProductComponent=React.createClass({render:function(){return{this.props.name}{this.props.price}{this.props.tax};}});varUnavailableProductComponent=React.createClass({render:function(){return{this.props.name}Unavailable;}});varBasketComponent=React.createClass({render:function(){Anatomy of the React component 96return

{this.props.headers.map(function(header){return{header}})}

;}});React.render(,$('div')[0]);CoffeeScriptAvailableProductComponent=React.createClassrender:->React.DOM.trnull,React.DOM.td(key:'name',@props.name)React.DOM.td(key:'price',@props.price)React.DOM.td(key:'tax',@props.tax)availableProductComponent=React.createFactory(AvailableProductComponent)UnavailableProductComponent=React.createClassrender:->React.DOM.trnull,React.DOM.tdkey:'name',@props.nameReact.DOM.tdkey:'unavailable'colSpan:2"Unavailable"unavailableProductComponent=React.createFactory(UnavailableProductComponent)BasketComponent=React.createClassrender:->React.DOM.divclassName:"basket",Anatomy of the React component 97React.DOM.tablekey:'table',React.DOM.theadkey:'head',@props.headers.map((header)->React.DOM.thkey:"th-\#{header}",header)React.DOM.tbodykey:'body',availableProductComponentkey:'available-product'name:"Product1"price:"$12.00"tax:"$1.00"unavailableProductComponentkey:'unavailable-product'name:"Product1"basketComponent=React.createFactory(BasketComponent)React.renderbasketComponent(headers:["Productname""Price""Tax"]),$("div")[0]


Recommended