├── Acessorios ├── README.md ├── flux-diagram-white-background.png └── flux-simple-f8-diagram-with-client-action-1300w.png ├── LICENSE ├── README.md ├── _config.yml ├── banner_livros2.png ├── fontes ├── App.js ├── App.test.js ├── Botao.js ├── README.md ├── Resultado.js ├── im1.png └── package.json ├── inicio ├── README.md ├── im1.png └── index.dev.html ├── react.png ├── react_redux ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── react.png ├── react_redux_f1.png ├── redux.png └── src │ ├── Actions.js │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── Botao.js │ ├── Container.js │ ├── Presentational.js │ ├── Reducer.js │ ├── Resultado.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ └── registerServiceWorker.js └── redux_form ├── .gitignore ├── README.md ├── __pycache__ └── authserver.cpython-36.pyc ├── authserver.py ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── react.png ├── redux_form.tar ├── redux_form_f1.png ├── redux_form_f2.png ├── rf.png └── src ├── Actions.js ├── App.css ├── App.js ├── App.test.js ├── Container.js ├── LoginForm.js ├── Presentational.js ├── Reducer.js ├── index.css ├── index.js ├── logo.svg └── registerServiceWorker.js /Acessorios/README.md: -------------------------------------------------------------------------------- 1 | ![](../react.png) 2 | # React! Don't Panic! 3 | 4 | [![](../banner_livros2.png)](https://www.lcm.com.br/site/#livros/busca?term=cleuton) 5 | 6 | ## Strike three: Má notícia nunca vem sozinha... 7 | 8 | É, meus amigos, a moda agora é criar frameworks Javascript. Isso é bom e ruim... É bom porque nos dá uma grande variedade de componentes e bibliotecas para usarmos em nossos projetos, e a maioria delas é open source. E é ruim porque são mais "roadmaps" para gerenciar! Sim, não raro, uma biblioteca é atualizada e quebra a compatibilidade com as outras, gerando uma grande dor de cabeça. 9 | 10 | O React é exatamente assim. 11 | 12 | Então, vou mostrar alguns dos "**Acessórios**" mais utilizados em aplicações React. Falarei rapidamente sobre sua funcionalidade e utilidade, dando alguns pequenos exemplos de código. Em outro artigo, mostrarei um exemplo mais concreto de aplicação, usando alguns desses "acessórios". 13 | 14 | ## Flux (Facebook) 15 | 16 | Flux é uma arquitetura de aplicação de fluxo unidirecional para o uso do React. 17 | 18 | ![](./flux-diagram-white-background.png) 19 | 20 | Complicou, não? Ok... Vamos tentar uma coisa mais simples... Toda aplicação Flux tem alguns *"papéis"* básicos: 21 | - **View**: UI. Onde o usuário interage com a aplicação; 22 | - **Store**: Armazena o estado da aplicação; 23 | - **Action**: Muda o estado da aplicação; 24 | - **Dispatcher**: Encaminha as ações às **stores**; 25 | 26 | Entendeu? Não? Dê uma olhada nessa figura, que é bem mais simples: 27 | 28 | ![](./flux-simple-f8-diagram-with-client-action-1300w.png) 29 | 30 | 1. O usuário digita um campo em um form e tecla ; 31 | 2. A **View** envia uma **Action** para o **Dispatcher**; 32 | 3. O **Dispatcher** envia a **Action** para as **Stores**; 33 | 4. As **Stores** atualizam o estado; 34 | 5. As **Views** refletem o novo estado (*). 35 | 36 | As Views detectam a mudança e obtém o novo estado. Na verdade, existe um tipo de componente, o **Container**, que atualiza o estado das views filhas, a cada mudança de estado das **Stores**. 37 | 38 | ## REDUX 39 | 40 | O **Flux** e o [**Redux**](https://redux.js.org) funcionam de maneira muito semelhante, implementando uma forma derivada de MVC no React. As diferenças são poucas, sendo que o **Flux** é um design pattern e uma biblioteca de classes e utilitários, e o **Redux** é um componente. 41 | 42 | ## Exemplo 43 | 44 | Há um exemplo simples de aplicação **Flux** que você pode acessar para ver como funciona: 45 | 46 | [https://github.com/facebook/flux/tree/master/examples/flux-todomvc](https://github.com/facebook/flux/tree/master/examples/flux-todomvc) 47 | 48 | Em breve, mostrarei mais acessórios e um exemplo funcional com o **Flux** 49 | -------------------------------------------------------------------------------- /Acessorios/flux-diagram-white-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/Acessorios/flux-diagram-white-background.png -------------------------------------------------------------------------------- /Acessorios/flux-simple-f8-diagram-with-client-action-1300w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/Acessorios/flux-simple-f8-diagram-with-client-action-1300w.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./react.png) 2 | # React! Don't Panic! 3 | 4 | [![](./banner_livros2.png)](https://www.lcm.com.br/site/#livros/busca?term=cleuton) 5 | 6 | A non-nonsense tutorial about [**React Library**](https://reactjs.org). 7 | 8 | - Strike one: [**OMG !**](./inicio/); 9 | - Strike two: [**Trabalho de gente grande**](./fontes/); 10 | - Strike three: [**Acessórios**](./Acessorios) 11 | - Strike four: [**React com Redux**](./react_redux) 12 | - Strike five: [**Formulários com Redux-form**](./redux_form) 13 | 14 | (c) 2018 [**Cleuton Sampaio**](https://github.com/cleuton). 15 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /banner_livros2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/banner_livros2.png -------------------------------------------------------------------------------- /fontes/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | import Botao from './Botao'; 5 | 6 | class App extends Component { 7 | render() { 8 | return ( 9 |
10 |
11 | logo 12 |

Welcome to React

13 |
14 | 15 |
16 | ); 17 | } 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /fontes/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import Botao from './Botao' 5 | import ReactTestUtils from 'react-dom/test-utils'; 6 | import { mount } from 'enzyme'; 7 | import Adapter from 'enzyme-adapter-react-16'; 8 | import { configure } from 'enzyme'; 9 | import { expect } from 'chai'; 10 | 11 | beforeEach(function() { 12 | global.fetch = jest.fn().mockImplementation(()=> { 13 | var p = new Promise((resolve,reject) => { 14 | const response = function() {return {"ip": "192.168.1.1","hostname": "No Hostname","city": "Rio de Janeiro","region": "Rio de Janeiro","country": "BR","loc": "-22.9876,-43.3207","org": "VIVO"}; } 15 | resolve({'json':response}); 16 | }) 17 | return p; 18 | } 19 | ) 20 | 21 | }) 22 | 23 | describe('Teste da interface', () => { 24 | it('deve aparecer o botão Pesquisar, sem erros', () => { 25 | // Sem enzyme: 26 | const div = document.createElement('div'); 27 | ReactDOM.render(, div); 28 | const buttons = div.getElementsByTagName("button"); 29 | //console.log(buttons[0].firstChild.data); 30 | expect(buttons[0].firstChild.data).to.be.equal('Pesquisar'); 31 | ReactDOM.unmountComponentAtNode(div); 32 | }); 33 | 34 | it('Ao ser clicado, deve retornar uma tabela', () =>{ 35 | // com enzyme: 36 | configure({ adapter: new Adapter() }); 37 | const wrapper = mount( 38 | 39 | ); 40 | const botao = wrapper.find('button'); 41 | botao.simulate('click'); 42 | setTimeout(function(){ 43 | expect(wrapper.find('table')).to.have.length(1); 44 | }, 2000); 45 | 46 | 47 | }); 48 | }); -------------------------------------------------------------------------------- /fontes/Botao.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Resultado from './Resultado'; 3 | 4 | class Botao extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = {'dados' : props.resultado}; 8 | // This binding is necessary to make `this` work in the callback 9 | this.handleClick = this.handleClick.bind(this); 10 | } 11 | 12 | handleClick() { 13 | 14 | fetch('http://ipinfo.io/json' 15 | ) 16 | .then(response => response.json()) 17 | .then(rjson => { 18 | this.setState({'dados':rjson}) 19 | }) 20 | 21 | 22 | } 23 | 24 | render() { 25 | return ( 26 |
27 | 30 | {this.state.dados != null && 31 | 32 | } 33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | export default Botao; -------------------------------------------------------------------------------- /fontes/README.md: -------------------------------------------------------------------------------- 1 | ![](../react.png) 2 | # React! Don't Panic! 3 | 4 | [![](../banner_livros2.png)](https://www.lcm.com.br/site/#livros/busca?term=cleuton) 5 | 6 | ## Strike two: Trabalho de "gente grande" 7 | 8 | Ok, no nosso primeiro tutorial, vimos o “jeitão” do React e criamos uma app “maneirinha”, certo? Mas como criar um projeto de verdade? Sim, com um ciclo de desenvolvimento completo: Desenvolvimento, Teste e Exportação para Produção? 9 | 10 | É isso que veremos agora. 11 | 12 | ### Trabalhando como gente grande 13 | 14 | A primeira versão que fizemos foi o equivalente àquela frase que ouvíamos quando crianças: *“Senta ali no cantinho e faça um desenho bem bonito!”* 15 | 16 | *Eita, lasqueira!* Convencemos o Chefe a usar React e ele agora quer que criemos um projeto de verdade… Calma! O pessoal do Facebook já pensou em tudo e criou uma “máquina de fazer salsicha” que gera uma app React. E não só isso: Ela cria um ciclo de desenvolvimento completo! Tudinho! 17 | 18 | Para começar, deixe-me perguntar uma coisa: Fez o “dever de casa”? Sim, lembra que eu pedi para instalar o NodeJS e o NPM? Ah, não lembra… Tudo bem… Vamos repetir. 19 | 20 | Prepare-se para ser “abduzido”: 21 | 22 | 1) Instale o nvm (https://github.com/creationix/nvm). Você precisa ter o NPM versão 5.2 ou superior, e o NodeJS versão 8.x; 23 | 2) Usando o NVM, instale a versão "stable" do NodeJS: nvm install 8.10.0; 24 | 3) Crie uma pasta; 25 | 4) Usando o NPX (vem com o NPM novo), digite os comandos: 26 | ``` 27 | npx create-react-app teste 28 | cd teste 29 | npm start 30 | ``` 31 | Agora, dê uma olhada na aplicação e examine sua estrutura. Abra a pasta "src" e veja o "App.js". 32 | 33 | O “create-react-app” criou uma aplicação completa, que utiliza o ciclo de desenvolvimento gerido pelo “npm”. Podemos executar as fases deste ciclo de vida com os comandos: 34 | 35 | • **Desenvolver** (```npm start```): Executa a aplicação em modo de desenvolvimento, utilizando um servidor web NodeJS. Se os arquivos forem alterados, a página é automaticamente recarregada, para desenvolvimento interativo; 36 | 37 | • **Testar** (```npm test```): Dispara um componente NodeJS chamado “Jest”, que executa sua aplicação dentro do NodeJS e não em um Navegador. Com isto, os scripts de teste podem ser executados; 38 | 39 | • **Preparar para a Produção** (```npm run build```): Prepara uma versão da aplicação para ser instalada em ambiente de produção, criando uma pasta “buid”. A aplicação é compilada e os arquivos minificados, otimizados para carga rápida. 40 | 41 | 42 | ### Ok, mas o que eu faço agora? 43 | 44 | Bom, se você já tem uma aplicação pronta, que é o nosso caso, é só modificar o arquivo “./src/App.js”, que o resto já está pronto! 45 | 46 | Olhe como nossa app ficará: 47 | 48 | ![](./im1.png) 49 | 50 | O trabalho é muito simples: 51 | 52 | 1. Separamos os dois componentes em arquivos JS: “Botao.js” e “Resultado.js”, colocando-os na pasta “src” do projeto React gerado; 53 | 2. Dentro do arquivo “src/App.js”, inserimos a renderização do componente principal, o “Botao”; 54 | 3. Dentro do arquivo “src/App.js”, importamos a classe “Botao”; 55 | 4. Dentro do arquivo “src/Botao.js”, importamos a classe “Resultado”; 56 | 5. Escrevemos casos de teste; 57 | 6. Testamos; 58 | 7. Fazemos o “build” final. 59 | 60 | Dentro do meu repositório no Github (https://github.com/cleuton/ReactDontPanic) há uma pasta “fontes” com os arquivos JS modificados. É só baixar e copiar para a pasta “src” do seu projeto gerado pelo “create-react-app”. 61 | 62 | ### Separar os componentes em seus próprios arquivos 63 | 64 | Crie dois arquivos: “Botao.js” e “Resultado.js”, copiando as respectivas classes para dentro deles. 65 | 66 | No início de cada arquivo, inclua esta linha, antes do código-fonte da classe respectiva: 67 | ``` 68 | import React, { Component } from 'react'; 69 | ``` 70 | E inclua esta linha final, após o código-fonte da classe respectiva: 71 | ``` 72 | export default ; 73 | ``` 74 | Por exemplo, eis o código da classe “Botao”: 75 | ``` 76 | import React, { Component } from 'react'; 77 | 78 | class Botao extends React.Component { 79 | constructor(props) { 80 | ... 81 | } 82 | 83 | handleClick() { 84 | ... 85 | } 86 | 87 | render() { 88 | ... 89 | } 90 | } 91 | 92 | export default Botao; 93 | ``` 94 | 95 | ### Renderizar o componente principal dentro de “App.js”: 96 | 97 | Lembra de como estava o nosso arquivo “index.html”, do tutorial anterior? Havia um comando assim: 98 | ``` 99 | ReactDOM.render( 100 | , 101 | document.getElementById('root') 102 | ); 103 | ``` 104 | Bom, isso não vai acontecer mais assim. No “esqueleto” de aplicação criado, há um arquivo “index.js” que já renderiza o componente “App” (que surpresa, não?): 105 | ``` 106 | ReactDOM.render(, document.getElementById('root')); 107 | registerServiceWorker(); 108 | ``` 109 | 110 | Então, como vamos renderizar nosso componente principal, o “Botao”? Simples! Dentro do método “render()” do componente “App”! 111 | 112 | 113 | ``` 114 | import Botao from './Botao'; // <----ESTA LINHA 115 | 116 | class App extends Component { 117 | render() { 118 | return ( 119 |
120 |
121 | logo 122 |

Welcome to React

123 |
124 | // <----E ESTA LINHA 125 |
126 | ); 127 | } 128 | } 129 | ``` 130 | As linhas marcadas foram as que eu inseri ou alterei. Se vamos utilizar um componente novo, então temos que importá-lo dentro do código, como fazemos em outras linguagens (Java, Python etc). 131 | 132 | ### Importar o componente filho 133 | 134 | O componente “Botao” renderiza também o componente “Resultado”, logo, deve ser importado para dentro dele. Acrescente a linha marcada logo abaixo do primeiro “import”: 135 | 136 | import React, { Component } from 'react'; 137 | import Resultado from './Resultado'; // <----ESTA LINHA 138 | 139 | class Botao extends React.Component { 140 | ... 141 | 142 | ### Iniciar a app em desenvolvimento 143 | 144 | Para subir sua aplicação em desenvolvimento, criando um Servidor local, que escuta na porta 3000, basta rodar o comando: 145 | ``` 146 | npm start 147 | ``` 148 | Na pasta raiz do projeto. 149 | 150 | Uma janela de navegador se abrirá com a página. Simples e prático. Se não abrir, ou se você inadvertidamente fechar o navegador, basta abrir outra janela e digitar: “http://localhost:3000”. 151 | 152 | 153 | ### Uma nota sobre apps Frontend 154 | 155 | O desenvolvimento de apps Web modernas é dividido em duas camadas fisicamente separadas: Frontend e Backend. Se você já trabalhou com Angular, conhece essa arquitetura. 156 | 157 | Todo código de conversação é executado no Frontend e o Backend é um mero fornecedor de recursos REST. 158 | 159 | Por isto, subimos um servidor web, em vez de abrir o arquivo HTML diretamente. 160 | 161 | Mas isto tem alguns problemas, como o CORS (Cross Origin Resource Sharing: https://pt.wikipedia.org/wiki/Cross-origin_resource_sharing). Eu não foi misturar os assuntos aqui, mas, se houver interesse, posso postar um artigo sobre isto. 162 | 163 | Em resumo, sua aplicação Frontend será instalada em um Servidor Web convencional, como um Apache ou Ngix, e a sua aplicação Backend, composta por RESTful services, será servida por outro host, como um Jboss (Se for Java), IIS (.NET) ou Apache (PHP). 164 | 165 | No nosso exemplo, o Backend já existe e pouco sabemos sobre ele. Conhecemos apenas a API. 166 | 167 | ### Testando 168 | 169 | Seguindo a prática de TDD (https://pt.wikipedia.org/wiki/Test_Driven_Development), o correto seria criamos scripts de teste, com vários cenários e casos de teste, ANTES de iniciarmos a codificação da nossa aplicação. A cada nova funcionalidade, podemos executar os cenários para ver se algo quebrou. 170 | 171 | Temos o comando: npm test, que utiliza o componente NodeJS “Jest” (https://facebook.github.io/jest/). Ele invocará os nossos scripts de teste em modo “watch” (sem abrir navegador). Poderemos ver quais testes passaram ou falharam. 172 | 173 | O “create-react-app” já cria um arquivo de teste para nós: “App.test.js”, que usa a sintaxe Jasmine (https://jasmine.github.io/2.0/introduction.html) para testar a aplicação: 174 | ``` 175 | import React from 'react'; 176 | import ReactDOM from 'react-dom'; 177 | import App from './App'; 178 | 179 | it('renders without crashing', () => { 180 | const div = document.createElement('div'); 181 | ReactDOM.render(, div); 182 | ReactDOM.unmountComponentAtNode(div); 183 | }); 184 | ``` 185 | Este teste é muito simples, apenas renderiza a aplicação e espera que não resulte em erro. Se houver um erro, o Jest mostrará isso. 186 | 187 | Se executarmos o teste: 188 | ``` 189 | npm test 190 | ``` 191 | Veremos o watch: 192 | 193 | ``` 194 | PASS src/App.test.js ✓ renders without crashing (24ms) 195 | 196 | Test Suites: 1 passed, 1 total 197 | Tests: 1 passed, 1 total 198 | Snapshots: 0 total 199 | Time: 0.998s, estimated 1s 200 | Ran all test suites related to changed files. 201 | 202 | Watch Usage 203 | › Press p to filter by a filename regex pattern. 204 | › Press t to filter by a test name regex pattern. 205 | › Press q to quit watch mode. 206 | › Press Enter to trigger a test run. 207 | ``` 208 | 209 | Eu não quero entrar no assunto de criação de casos de teste aqui, mas, para sermos levados a sério, precisamos fazer algo melhor! 210 | 211 | Vamos testar realmente se o botão aparece e se, ao ser clicado, a tabela aparece. São dois testes. 212 | 213 | Para começar, vamos instalar mais alguns componentes… Você notou que há uma pasta “node_modules” no seu projeto, não? Esta pasta contém as dependências dos componentes, baixadas pelo “npm”. Há também um arquivo de controle, chamado: “package.json”, onde todas as dependências estão listadas. 214 | 215 | Vamos acrescentar essas dependências: 216 | ``` 217 | "devDependencies": { 218 | "chai": "^4.1.2", 219 | "enzyme": "^3.3.0", 220 | "enzyme-adapter-react-16": "^1.1.1" 221 | } 222 | ``` 223 | São dependências utilizadas apenas em desenvolvimento e teste. Quando você fizer o “build”, não serão levadas para a versão final. Daí a propriedade: “devDependencies”. São 3: “chai”, “enzyme” e “enzyme-adapter-react-16”. Você pode editar o arquivo “package.json” ou então executar o comando: 224 | ``` 225 | npm install –save-dev chai enzyme enzyme-adapter-react-16 226 | ``` 227 | O “npm” baixará as dependências, salvando-as dentro da pasta “node_modules”, e alterará o arquivo “package.json” para você. 228 | 229 | Agora, vamos alterar o arquivo “App.test.js” para nossa nova versão: 230 | ``` 231 | import React from 'react'; 232 | import ReactDOM from 'react-dom'; 233 | import App from './App'; 234 | import Botao from './Botao' 235 | import ReactTestUtils from 'react-dom/test-utils'; 236 | import { mount } from 'enzyme'; 237 | import Adapter from 'enzyme-adapter-react-16'; 238 | import { configure } from 'enzyme'; 239 | import { expect } from 'chai'; 240 | 241 | beforeEach(function() { 242 | global.fetch = jest.fn().mockImplementation(()=> { 243 | var p = new Promise((resolve,reject) => { 244 | const response = function() {return {"ip": "192.168.1.1","hostname": "No Hostname","city": "Rio de Janeiro","region": "Rio de Janeiro","country": "BR","loc": "-22.9876,-43.3207","org": "VIVO"}; } 245 | resolve({'json':response}); 246 | }) 247 | return p; 248 | } 249 | ) 250 | 251 | }) 252 | 253 | describe('Teste da interface', () => { 254 | it('deve aparecer o botão Pesquisar, sem erros', () => { 255 | // Sem enzyme: 256 | const div = document.createElement('div'); 257 | ReactDOM.render(, div); 258 | const buttons = div.getElementsByTagName("button"); 259 | //console.log(buttons[0].firstChild.data); 260 | expect(buttons[0].firstChild.data).to.be.equal('Pesquisar'); 261 | ReactDOM.unmountComponentAtNode(div); 262 | }); 263 | 264 | it('Ao ser clicado, deve retornar uma tabela', () =>{ 265 | // com enzyme: 266 | configure({ adapter: new Adapter() }); 267 | const wrapper = mount( 268 | 269 | ); 270 | const botao = wrapper.find('button'); 271 | botao.simulate('click'); 272 | setTimeout(function(){ 273 | expect(wrapper.find('table')).to.have.length(1); 274 | }, 2000); 275 | 276 | 277 | }); 278 | }); 279 | ``` 280 | 281 | Nossos dois testes estão incluídos em um “describe”, que separa cenários de teste. Cada “it” é um caso de teste separado. 282 | 283 | No primeiro caso de teste, eu usei o próprio “DOM” dos elementos para saber se foi baixado um botão HTML cujo texto é “Pesquisar”. 284 | ``` 285 | expect(buttons[0].firstChild.data).to.be.equal('Pesquisar'); 286 | ``` 287 | Se não existir o botão ou se o label for diferente de “Pesquisar”, o caso de teste falhará. 288 | 289 | No segundo, eu utilizei o componente “enzyme” (https://github.com/airbnb/enzyme) que facilita o teste de componentes React, permitindo interagir com propriedades, métodos e eventos. 290 | 291 | Nós criamos uma instância de ReactWrapper que encapsula nosso Componente: 292 | ``` 293 | const wrapper = mount( 294 | 295 | ); 296 | ``` 297 | Obtemos uma referência para o elemento “button” e simulamos o clique: 298 | ``` 299 | const botao = wrapper.find('button'); 300 | botao.simulate('click'); 301 | ``` 302 | Depois, é só verificar se foi gerada uma “``````” dentro do nosso Componente: 303 | ``` 304 | expect(wrapper.find('table')).to.have.length(1); 305 | ``` 306 | Você deve ter notado o "beforeEach()", não? Eu tive que incluir isso... É como o JUnit, é executado antes de cada teste. Por quê? Porque quando eu clico no botão, eu faço um "fetch()" para o site. E isso vai falhar no teste! 307 | 308 | Ao "mockar" o "fetch" eu consigo executar testes verdeiramente unitários. Nesta parte, eu estou mocando o comportamento da função global "fetch" retornando um JSON simulado (na verdade, eu retorno um objeto que contém uma função "toJson()", para ficar igual ao que o "fetch" verdadeiro retornaria). 309 | 310 | E tive que incluir um "setTimeout()" no código de teste, para rodar o "mock" antes de testar a resposta. 311 | 312 | Se você deixar executando o “npm test”, qualquer alteração será imediatamente detectada e os testes serão reexecutados. 313 | 314 | Nesse exemplo, estamos fazendo o request GET a cada teste. Podemos “mockar” o componente, simulando o request, o que torna o teste efetivamente unitário. 315 | 316 | ### E voilà 317 | 318 | Passou em todos os testes? Legal! Então vamos fazer um build para instalar em “Produção”: 319 | ``` 320 | npm run build 321 | ``` 322 | E pronto! Na pasta “build” você verá que não existe nada de React! Os Javascript estão minificados e prontos. 323 | 324 | O próprio “build” lhe dá uma dica para servir seu conteúdo. Se quiser mostrar a versão de produção, instale um módulo chamado “serve” (como componente Global): 325 | ``` 326 | npm install -g serve 327 | ``` 328 | E mande rodar: 329 | ``` 330 | serve -s build 331 | ``` 332 | Depois, aponte seu navegador para “http://localhost:5000”. 333 | 334 | Não é um webserver “production grade”, mas serve para gerar uma versão de integração, que pode ser acessada até na rede local. 335 | 336 | Bão… 337 | 338 | Conforme prometido, neste segundo tutorial, mostrei como construir uma app de verdade, em ambiente pré compilado, como testar e como fazer um “deploy” para produção. 339 | 340 | No próximo tutorial, vou criar outra app com React e Python (Flask) que deverá conter mais interações, como formulários, por exemplo. 341 | 342 | Até lá… 343 | 344 | *Cleuton Sampaio, M.Sc.* 345 | -------------------------------------------------------------------------------- /fontes/Resultado.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Resultado extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = {'dados' : props.dados} 7 | } 8 | 9 | render() { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
IP{this.state.dados.ip}
Hostname{this.state.dados.hostname}
Cidade{this.state.dados.city}
Região{this.state.dados.region}
País{this.state.dados.country}
Loc{this.state.dados.loc}
Organização{this.state.dados.org}
22 | ) 23 | } 24 | } 25 | 26 | export default Resultado; -------------------------------------------------------------------------------- /fontes/im1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/fontes/im1.png -------------------------------------------------------------------------------- /fontes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teste", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.2.0", 7 | "react-dom": "^16.2.0", 8 | "react-scripts": "1.1.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test --env=jsdom", 14 | "eject": "react-scripts eject" 15 | }, 16 | "devDependencies": { 17 | "chai": "^4.1.2", 18 | "enzyme": "^3.3.0", 19 | "enzyme-adapter-react-16": "^1.1.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /inicio/README.md: -------------------------------------------------------------------------------- 1 | ![](../react.png) 2 | # React! Don't Panic! 3 | 4 | [![](../banner_livros2.png)](https://www.lcm.com.br/site/#livros/busca?term=cleuton) 5 | 6 | ## Strike one: OMG! 7 | 8 | Conforme prometido, esse é o primeiro tutorial de desenvolvimento Web com a biblioteca React, criada pelo Facebook. 9 | 10 | Você verá muita coisa nova, pois o moderno desenvolvimento Web, separado em “frontend” e “backend”, impõe grande mudança de perspectiva. Se você se acostumou a utilizar frameworks monolíticos, como: PHP, .NET ou JSF, provavelmente estranhará muito. Mas, se já utilizou Angular, por exemplo, já está acostumado. 11 | 12 | Normalmente, usamos um conjunto de ferramentas e um workflow de desenvolvimento de apps Web “frontend”. No caso do React, usamos o NodeJS, o NPM, o Webpack, o Babel e outras. E temos um workflow de desenvolvimento, teste e “build” de “frontend”. 13 | 14 | Há um componente free chamado “create-react-app” que facilita tudo para você: 15 | 16 | https://github.com/facebook/create-react-app 17 | 18 | Porém, pode ser um pouco confuso ver tudo isso junto, então, neste primeiro tutorial, quero focar no uso do React em si, ou seja, como você desenvolve uma página com React, e não no resto, que mostrarei em outra oportunidade. 19 | 20 | 21 | ### Vamos começar por pequenas partes. 22 | 23 | *"Minha primeira página React"* 24 | 25 | Ok. Vamos começar beeem devagar. Acesse o repositório no Github e baixe o projeto “ReactDontPanic”: 26 | 27 | https://github.com/cleuton/ReactDontPanic 28 | 29 | Você pode clonar o repositório ou baixar um Zip. 30 | 31 | Entre na pasta que baixou e abra a subpasta “inicio”, dê um duplo clique no arquivo [**“index.html”**](./index.html). É uma pequena aplicação React, que vou analisar com você. 32 | 33 | Esta aplicação é beeem simples! Ela acessa um RESTful service disponível na rede, o “IpInfo.io” (http://ipinfo.io). Ele retorna as informações do seu endereço ip. Veja um exemplo de uso desta página: 34 | 35 | ![](./im1.png) 36 | 37 | Inicialmente, a página mostra apenas com o botão “Pesquisar”. Ao clicar no Botão, é feito um request GET e o estado da página é atualizado, mostrando uma tabela com as informações retornadas. 38 | 39 | 40 | ### “Zoiando” o código 41 | 42 | Abra o arquivo “index.html” com um editor de textos (Gedit, Notepad, Visual Studio Code ou qualquer outro editor de arquivos gerais). 43 | 44 | Nota: Este exemplo é apenas para demonstração! Não está otimizado para Produção!. Ele invoca o compilador “Babel” (https://babeljs.io/) para compilar o código Javascript “on the fly”, incluindo as expressões JSX, o que é muito lento. No próximo artigo, mostrarei como fazer uma versão mais profissional. 45 | 46 | Todas as bibliotecas Javascript que você precisa importar estão na seção “head”: 47 | 48 | ``` 49 | 50 | 51 | React! Don't panic! 52 | 53 | 54 | 55 | 56 | ```` 57 | 58 | E o código Javascript da nossa aplicação está dentro de um único tag “``` 75 | ``` 76 | Fica bem mais fácil de entender como programar em React, não? Essa não é a melhor maneira de criar um site com React, mas é a mais fácil para começarmos. 77 | 78 | Nossa página é muito simples. Ela tem um botão que, ao ser clicado, faz um request AJAJ (Async Javascript And JSON) e formata a resposta como uma tabela. Simples. Só estamos trabalhando no Frontend, que vai rodar dentro do seu navegador. 79 | 80 | ### Elementos e Componentes 81 | 82 | O React não separa renderização de controle, ou seja, ele não força um modelo MVW, como o Angular (thanks God!). 83 | 84 | No Angular, a lógica de controle e a de renderização ficam em arquivos totalmente separados. No React, os conceitos são separados em componentes de baixo acoplamento que já embutem tudo o que é necessário para prover a função: manutenção de estado, eventos, preparação e renderização. 85 | 86 | Um componente React é autocontido e totalmente independente, podendo ser reutilizado em diversas outras aplicações. 87 | 88 | Customizações não precisam ser feitas em tags HTML, mas em CSS, e o React permite isso. 89 | 90 | Um componente React utiliza Elementos (com “E” maiúsculo) para renderizar as informações. 91 | 92 | Recaptulando… Uma página React é criada por Componentes, que usam outros Componentes e Elementos para criar a UI. 93 | 94 | Na nossa página, temos apenas dois componentes: “Botao” e “Resultado”. Consegue identificá-los no código? Não? Aqui estão: 95 | 96 | class Botao extends React.Component { 97 | constructor(props) { 98 | ... 99 | } 100 | 101 | handleClick() { 102 | ... 103 | } 104 | 105 | render() { 106 | ... 107 | } 108 | } 109 | 110 | class Resultado extends React.Component { 111 | constructor(props) { 112 | ... 113 | } 114 | 115 | render() { 116 | ... 117 | } 118 | } 119 | 120 | São duas classes (Javascript ES6: https://googlechrome.github.io/samples/classes-es6/) derivadas de “React.Component”. Em ambas eu acrescentei um “construtor” e alguns outros métodos. Dê uma boa olhada no código! 121 | 122 | Ok, estes são os Componentes, e quais são os Elementos? Onde está o HTML que eu mostrei no exemplo? Calma! Procure localizar as quatro linhas finais do script, onde está este código: 123 | ``` 124 | ReactDOM.render( 125 | , 126 | document.getElementById('root') 127 | ); 128 | ``` 129 | Ele está inserindo um tag “``````” dentro de um tag cujo id é “root”, e esse tag é uma “```
```”, no início da página: 130 | 131 | ``` 132 | 133 |
134 | ```` 135 | 136 | Mas que raios de tag “Botao” é esse? Será que tem algo a ver com o nosso componente “Botao”? Claro que sim! Cada classe derivada de “React.Component” pode ser utilizada como Elemento HTML! Podemos renderizar Componentes, Elementos ou uma combinação de ambos! 137 | 138 | Procure a implementação do método “render()” da classe “Resultado”: 139 | 140 | ``` 141 | render() { 142 | return ( 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 |
IP{this.state.dados.ip}
Hostname{this.state.dados.hostname}
Cidade{this.state.dados.city}
Região{this.state.dados.region}
País{this.state.dados.country}
Loc{this.state.dados.loc}
Organização{this.state.dados.org}
154 | ) 155 | } 156 | ``` 157 | 158 | Este método retorna um HTML. Notou algo estranho? Não? Cadê as aspas? O HTML deveria ser um string, não? O Javascript deveria dar erro! 159 | 160 | Isso tem a ver com o JSX (https://facebook.github.io/jsx/), que é compilado pelo Babel! 161 | 162 | Vamos voltar só um pouquinho… Notou que o script tag da nossa aplicação especifica um argumento “type”? Qual é ele? 163 | 164 | ``` 165 | 166 | ``` 167 | 168 | 169 | Sim, é “text/babel”, ou seja, o Babel vai analisar nossa página e compilar todo o código em Javascript comum. Isso também é conhecido como “transpiler” ou “source-to-source” compiler. Ele pega um fonte e gera outro. Assim, as expressões JSX também são transformadas em comandos Javascript comuns. 170 | 171 | Então, voltando aos Componentes e Elementos, o nosso Componente “Resultado” renderiza um Elemento JSX “``````”, criado dinamicamente! 172 | 173 | Mas os componentes podem renderizar Elementos e também outros Componentes! Veja só o método “render()” do componente “Botao”: 174 | 175 | ``` 176 | render() { 177 | return ( 178 |
179 | 182 | {this.state.dados != null && 183 | 184 | } 185 |
186 |
187 | ); 188 | } 189 | ``` 190 | 191 | Ele renderiza uma “```
```”, que é um elemento JSX, que contém um “``` 216 | {this.state.dados != null && // <---CONDICIONAL 217 | 218 | } 219 |
220 |
221 | ); 222 | } 223 | ``` 224 | 225 | 4 - O evento “onClick” é associado ao método “handleClick()”, do Componente “Botao”; 226 | 227 | 5 - Como havia um condicional, o componente “``````” não será renderizado pelo componente “Botao”, pois, no início a propriedade “dados”, do estado é nula: 228 | 229 | 5.1 - Nós renderizamos o componente “Botao”, na carga da página HTML, com o parâmetro “resultado” como null: 230 | ``` 231 | ReactDOM.render( 232 | , 233 | document.getElementById('root') 234 | ); 235 | ``` 236 | 237 | 5.2 - O construtor do componente “Botao” inicia o estado interno (this.state) com o valor da propriedade “resultado”, que foi passada como null: 238 | 239 | ``` 240 | class Botao extends React.Component { 241 | constructor(props) { 242 | super(props); 243 | this.state = {'dados' : props.resultado}; 244 | ``` 245 | 246 | 5.3 - Por isto, a renderização inicial da página só apresenta o botão, e nada mais. 247 | 248 | 6 - O usuário clica no botão, o evento é capturado e a função “this.handleClick()” é invocada: 249 | 250 | ``` 251 | handleClick() { 252 | 253 | fetch('http://ipinfo.io/json' 254 | ) 255 | .then(response => response.json()) 256 | .then(rjson => { 257 | this.setState({'dados':rjson}) 258 | }) 259 | 260 | } 261 | ``` 262 | 263 | 7 - Como modificamos o estado interno do componente “Botao”, ele será renderizado novamente. Desta vez, a propriedade “dados”, do seu estado, conterá um objeto JSON, logo, o condicional da renderização resultará em verdadeiro, incluindo o componente “``````”: 264 | 265 | ``` 266 | {this.state.dados != null && 267 | 268 | } 269 | ``` 270 | 271 | Não podemos mudar a aparência de um Componente já renderizado. Se for necessário, precisamos renderizá-lo novamente, e isto acontece sempre que alteramos o seu estado interno (this.setState()). 272 | 273 | ### AJAJ 274 | 275 | Podemos usar qualquer framework para acesso assíncrono, como o Jquery, por exemplo. Mas o React oferece a API “fetch”, para consumo de informações REST: https://facebook.github.io/react-native/docs/network.html 276 | 277 | No nosso caso, fizemos exatamente isso: 278 | ``` 279 | fetch('http://ipinfo.io/json' 280 | ) 281 | .then(response => response.json()) 282 | .then(rjson => { 283 | this.setState({'dados':rjson}) 284 | }) 285 | ``` 286 | 287 | O método “fetch()” retorna uma Promise (https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Promise). Podemos passar uma função para essa Promise. O código acima está usando a sintaxe lambda do ES6, mas poderíamos reescrevê-lo assim: 288 | 289 | ``` 290 | fetch('http://ipinfo.io/json' 291 | ) 292 | .then(function(response) { 293 | return response.json() 294 | .then(function(rjson) { 295 | this.setstate({'dados':rjson}); 296 | }); 297 | } 298 | ) 299 | ``` 300 | 301 | E o que acontece depois? Nós mudamos o estado interno do componente “Botao” (método “setState()”), isto forçara a recriação e renderização, logo, seu método “render()” será executado e, como agora a propriedade “dados” está preenchida, o componente “Resultado” será renderizado. 302 | 303 | O componente “Resultado” recebe o estado interno do componente “Botao”. Veja só como o “Botao” o renderiza: 304 | 305 | ``` 306 | {this.state.dados != null && 307 | 308 | } 309 | ``` 310 | 311 | Como “this.state.dados”, do componente “Botao”, agora contém os resultados recebidos do “fetch()”, então será diferente de “null”, logo, o tag “``````” será renderizado. E ele recebe um argumento “dados”, que conterá o JSON que recebemos. 312 | 313 | No construtor do componente “Resultado” ele pega o argumento recebido e armazena em seu próprio estado interno: 314 | 315 | ``` 316 | constructor(props) { 317 | super(props); 318 | this.state = {'dados' : props.dados} 319 | } 320 | ``` 321 | 322 | Todo componente recebe um argumento “props” em seu construtor, de onde pode acessar os argumentos utilizados no Tag. 323 | 324 | Quando o componente “Resultado” for renderizar a tabela, ele pegará os valores que estão em seu estado interno: 325 | 326 | ``` 327 | render() { 328 | return ( 329 |
330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 |
IP{this.state.dados.ip}
Hostname{this.state.dados.hostname}
Cidade{this.state.dados.city}
Região{this.state.dados.region}
País{this.state.dados.country}
Loc{this.state.dados.loc}
Organização{this.state.dados.org}
340 | ) 341 | } 342 | ``` 343 | 344 | Muito importante: Quando você colocar dentro de um argumento de Elemento JSX ou Componente um valor que seja Javascript, não use aspas! Use chaves “{ }”. 345 | 346 | ### O elefante é grande… Vamos comendo aos pedacinhos! 347 | 348 | Há muito mais coisas para aprendermos a usar o React corretamente. Mas aqui, vimos os primeiros passos, e com uma app que consome um serviço REST. 349 | 350 | No próximo tutorial, vou mostrar como criar uma app de produção, que pré-compila o Javascript. 351 | 352 | **Cleuton Sampaio, M.Sc.** 353 | 354 | -------------------------------------------------------------------------------- /inicio/im1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/inicio/im1.png -------------------------------------------------------------------------------- /inicio/index.dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React! Don't panic! 6 | 7 | 8 | 9 | 10 | 11 |
12 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/react.png -------------------------------------------------------------------------------- /react_redux/README.md: -------------------------------------------------------------------------------- 1 | ![](./react.png) 2 | # React! Don't Panic! 3 | 4 | [![](../banner_livros2.png)](https://www.lcm.com.br/site/#livros/busca?term=cleuton) 5 | 6 | ![](./redux.png) 7 | 8 | ## Strike four: React com Redux 9 | 10 | **Redux** é uma biblioteca para gestão de estado em aplicações Javascript. 11 | 12 | Só que, em torno do Redux, vários outros componentes e bibliotecas foram criados, como: 13 | - Redux **Form**: Um gerenciador de estado de forms usando Redux; 14 | - Redux **Router**: Para gerenciar suas rotas de aplicação dentro de uma store do Redux; 15 | - Redux **Dialog**: Componente para manter estado de diálogos em stores do Redux; 16 | 17 | Podemos estender esta lista interminávelmente... Para ter uma ideia, consulte o [**ecossistema Redux**](https://redux.js.org/introduction/ecosystem). 18 | 19 | ## E o Flux? 20 | 21 | Bom, pode ser meio complicado de entender... Redux é uma implementação do padrão [**Flux**](https://facebook.github.io/flux/docs/overview.html)? Sim e não, de acordo com a [documentação deles](https://redux.js.org/introduction/prior-art#flux). 22 | 23 | O Redux foi inspirado pelo padrão arquitetural do Flux, e segue alguns de seus preceitos, como o controle centralizado de estado, mas, ao contrário do Flux, o Redux prega que os "**reducers**" sejam funções "**puras**" e que não modifiquem o estado, apenas gerem um novo objeto. 24 | 25 | O Redux pode ser utilizado com outras bibliotecas Javascript, como [**Angular**](https://github.com/angular-redux/store) ou [**Ember**](http://www.ember-redux.com/). 26 | 27 | ## Mas eu usaria Redux para quê? 28 | 29 | Imagine uma aplicação moderna Javascript. Imaginou? Existem vários **Componentes**, cada um com seu **Estado**, que pode ser alterado por vários **Serviços** em operações **Assíncronas**. Continou imaginando? O usuário pode executar **Ações** que, por sua vez, disparam **Eventos** e que podem alterar os **Estados** individuais dos componentes. E o ciclo se repete. 30 | 31 | Acabamos com o estado replicado ou fragmentado entre componentes e objetos diferentes ao longo da nossa página. 32 | 33 | Com o Redux, temos uma **Store** que armazena nosso estado. Usamos um tipo de função especial, o **Reducer** para executar ações e gerar novo estado. Qualquer mudança de estado, terá que ser realizada por uma **Ação** documentada, e processada pelo **Reducer**. 34 | 35 | ### Mas eu preciso MESMO usar Redux? 36 | 37 | É verdade que nem toda aplicação **React** precisa usar o Redux. Ele acrescenta complexidade e indireção à sua já complexa aplicação React. Podemos dizer que o uso do Redux representa um *trade-off* que você deverá fazer, ou seja, acrescenta complexidade em troca de gerenciar melhor o estado e suas transições. 38 | 39 | Há várias discussões a respeito do uso ou não do Redux junto com o React (ou com outros frameworks frontend). Eu gosto de me apegar a uma filosofia: **Não use uma tecnologia só porque é moda**. Eis alguns artigos que falam sobre o uso do Redux junto com o React: 40 | 41 | - ["**You Probably Don’t Need Redux**"](https://medium.com/@blairanderson/you-probably-dont-need-redux-1b404204a07f) (Você provavelmente não precisa do Redux); 42 | - ["**When to use Redux**"](https://medium.com/@fastphrase/when-to-use-redux-f0aa70b5b1e2) (Quando utilizar o Redux); 43 | - ["**You Might Not Need Redux**"](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367) (Você pode não precisar do Redux). 44 | 45 | Algumas das razões para usar Redux: 46 | 47 | 1. Você tem um estado compartilhado com muitos componentes, como estado de conversação ou sessão; 48 | 2. Você tem componentes globais que se repetem em várias partes da sua UI, como Tabs por exemplo; 49 | 3. Há muitas propriedades (props) sendo passadas por toda uma hierarquia de componentes; 50 | 4. O usuário pode sair e voltar para sua aplicação e você precisa guardar o estado. 51 | 52 | Geralmente, aplicações de interações simples, como esta que vou mostrar, não precisam nem do React, quanto mais do Redux. 53 | 54 | ## Antes de começar 55 | 56 | Você pode criar uma app com o npx, como eu ensinei em [**Trabalhando como gente grande**](../fontes) ou pode usar a app que você já criou. Depois, instale o Redux e algumas "cositas" mais: 57 | ``` 58 | npm install --save redux 59 | npm install --save react-redux 60 | npm install --save redux-thunk 61 | ``` 62 | 63 | ## Principais mudanças na aplicação para usar Redux 64 | 65 | A aplicação que vou demonstrar é a mesma que venho utilizando desde o início. Ela busca as informações relativas ao seu Endereço IP. 66 | 67 | ![](./react_redux_f1.png) 68 | 69 | Para começar, precisamos transformar nosso estado em um script Json. Precisamos poder "escriptar" nosso estado. No meu caso, o estado pode ser reduzido a isto: 70 | 71 | ``` 72 | { 73 | aguardando: true/false, 74 | dados: {dados sobre o endereço IP} 75 | } 76 | ``` 77 | 78 | Não me interessa detalhar o estado da propriedade "dados", já que ela é utilizada apenas por um único componente React ([**Resultado.js**](./src/Resultado.js)). 79 | 80 | ### Vamos criar ações 81 | 82 | Minha aplicação vai trabalhar com requisição assíncrona, através de XmlHttpRequest, portanto, manda a boa prática que eu tenha duas ações: Uma para iniciar o request e outra para quando os resultados chegarem. 83 | 84 | O arquivo [**Actions.js**](./src/Actions.js) contém as definições das minhas ações e os "*action creators*". 85 | 86 | Se examinar o arquivo, verá que só estou exportando os nomes das ações e uma função chamada "fetchIp()", que invoca as duas ações em tempos diferentes. Não estou exportando as duas ações individuais. 87 | 88 | Todo **Action creator** Redux precisa retornar um objeto simples, com uma propriedade **Type**. É isto que as funções "requestIp()" e "receiveIp()" fazem. Já a função "fetchIp()" se parece com um Action creator, mas ela retorna uma **function**!!! Isso está errado! 89 | 90 | ### Thunk actions 91 | 92 | Segundo a Wikipedia, [**Thunk**](https://en.wikipedia.org/wiki/Thunk) é uma subrotina que "injeta" código em outra subrotina, por exemplo, para processar o resultado de operações assíncronas. Para processar operações assíncronas com o Redux, eu preciso criar uma **Thunk action**, e, para isto, preciso do *Middleware* [**redux-thunk**](https://github.com/gaearon/redux-thunk). 93 | 94 | Para instalar o *Middleware* basta abrir a pasta do projeto, depois de rodar ```npm install``` e digitar: 95 | ``` 96 | npm install --save redux-thunk 97 | ``` 98 | Depois, precisamos alterar o arquivo [**index.js**](./src/index.js) para adicionar o *Middleware*: 99 | ``` 100 | import { createStore, applyMiddleware } from 'redux'; 101 | import thunk from 'redux-thunk'; 102 | ... 103 | ... applyMiddleware(thunk) 104 | ``` 105 | 106 | Como podem ver no código, a função "fetchIp()" é um **Thunk Action Creator** e ela invoca as duas outras ações: "requestIp()" e "receiveIp()", em dois momentos diferentes, utilizando o objeto "**dispatch**": 107 | ``` 108 | export function fetchIp() { 109 | return function (dispatch) { 110 | console.log('invoked!!!') 111 | dispatch(requestIp()) 112 | console.log('fetching...') 113 | return fetch(url) 114 | .then( 115 | response => response.json(), 116 | error => console.log('An error occurred.', error) 117 | ) 118 | .then(json => { 119 | console.log('again! ' + JSON.stringify(json)) 120 | dispatch(receiveIp(json)) 121 | } 122 | ) 123 | } 124 | } 125 | ``` 126 | 127 | ### Reducer 128 | 129 | O **Reducer** é o *coração* do Redux! Ele processa as ações e gera o novo estado baseado nelas. O arquivo [**Reducer.js**](./src/Reducer.js) define o nosso reducer. Na verdade, você pode ter mais de um reducer e depois combiná-los em um "*root reducer*". Para facilitar, eis o código do Reducer: 130 | 131 | ``` 132 | import { 133 | REQUEST_IP, 134 | RECEIVE_IP 135 | } from './Actions' 136 | 137 | const estadoInicial = { 138 | aguardando: false, 139 | dados: null 140 | }; 141 | 142 | function atualizarDados(state = estadoInicial, action) { 143 | switch(action.type) { 144 | case REQUEST_IP: 145 | return Object.assign({}, state, { 146 | aguardando: true, 147 | dados: null 148 | } 149 | ) 150 | case RECEIVE_IP: 151 | return Object.assign({}, state, { 152 | aguardando: false, 153 | dados: action.dados 154 | }) 155 | default: 156 | return state; 157 | } 158 | } 159 | 160 | export default atualizarDados; 161 | ``` 162 | 163 | A função "atualizarDados()" é o meu reducer. Ela recebe um **Estado** e uma **Ação** e retorna um **Novo estado**. Reducers devem ser [**Pure functions**](https://en.wikipedia.org/wiki/Pure_function), ou seja: 164 | - Dados os mesmos argumentos, deve retornar sempre os mesmos valores (uso de números aleatórios ou de informação de tempo não é possível); 165 | - Não produz efeitos colaterais (não deve sequer logar informações); 166 | 167 | Um reducer pode receber um estado inicial (graças à característica de valor default para parâmetros). Se nenhuma ação foi informada, então deve simplesmente retornar o estado que recebeu. 168 | 169 | **Object.assign()** 170 | 171 | Este método serve para copiar as propriedades enumeráveis de um ou mais objetos origem, para um novo objeto destino. 172 | 173 | ### Criando a store 174 | 175 | No Redux, mantemos nosso estado em uma **Store**. Ela deve ser instanciada no arquivo [**index.js**](./src/index.js) da aplicação: 176 | ``` 177 | import { Provider } from 'react-redux'; 178 | import atualizarDados from './Reducer'; 179 | 180 | 181 | const store = createStore(atualizarDados, 182 | applyMiddleware(thunk)); 183 | ``` 184 | Não só criamos a instância da **Store** como também aplicamos o nosso *Middleware* **Thunk**. 185 | 186 | Para que os componentes tenham acesso à **Store** precisamos "embrulhar" nosso componente principal, [**App**](./src/App.js) em um Componente **Provider**: 187 | 188 | ``` 189 | import { Provider } from 'react-redux'; 190 | import App from './App'; 191 | 192 | render( 193 | 194 | 195 | , 196 | document.getElementById('root') 197 | ); 198 | ``` 199 | 200 | ### Componentes Container e Presentation 201 | 202 | O Redux introduz um padrão de projeto que consiste em separar os componentes em **Container** e **Presentation**. De acordo com a [**documentação**](https://redux.js.org/basics/usage-with-react#presentational-and-container-components) do Redux, o componente **Container** se preocupa em COMO as coisas funcionarão, e está ciente do Redux, sendo um "listener" da **Store**. Ele dispara as ações e repassa partes do **Estado** para os componentes **Presentation**. 203 | 204 | Um componente **Presentation** se preocupa com a APARÊNCIA das coisas, e não está ciente do Redux. Ele recebe dados através de **props** e também recebe **Callbacks** através delas. Ele não interage com o Redux e nem com a **Store**. Qualquer estado interno que ele mantenha, tem que ser derivado do que ele recebeu do **Container**. 205 | 206 | Eu tive que separar o meu componente original *App* em dois. Criei um componente **Container** ([Container.js](./src/Container.js)) e esse é o componente que eu uso dentro de [**App.js**](./src/App.js), o componente principal da minha aplicação, que é renderizado dentro do [**index.js**](./src/index.js). 207 | 208 | Meu componente **Container** tem que fazer algumas "coisinhas" especiais, por exemplo, se conectar à **Store** e mapear o estado e as ações em propriedades, para os componentes **Presentation**. Eis o código-fonte: 209 | ``` 210 | import { connect } from 'react-redux' 211 | import { fetchIp } from './Actions' 212 | import Presentational from './Presentational' 213 | 214 | const mapStateToProps = state => ( 215 | {dados: state.dados} 216 | ); 217 | 218 | const mapDispatchToProps = dispatch => ({ 219 | clickBotao: () => dispatch(fetchIp()) 220 | }) 221 | 222 | export default connect( 223 | mapStateToProps, 224 | mapDispatchToProps 225 | )(Presentational) 226 | 227 | ``` 228 | Ele não retorna nenhuma expressão **JSX**, já que esse papel é dos componentes **Presentation**. Ele usa a função "connect()", do **react-redux** para retornar um objeto conectado à **Store**. E eu passo alguns argumentos importantes: 229 | - A lista de propriedades que os componentes receberão (mapeada a partir do **Estado**); 230 | - A lista de *callbacks* que os componentes receberão; 231 | - O nome do componente principal **Presentation**. 232 | 233 | ### Componentes de apresentação 234 | 235 | Eu criei um componente [**Presentational.js**](./src/Presentational.js) para abrigar os componentes de apresentação, como o [**Botao.js**](./src/Botao.js) por exemplo. 236 | ``` 237 | import React from 'react'; 238 | import logo from './logo.svg'; 239 | import './App.css'; 240 | import Botao from './Botao'; 241 | 242 | const Presentational = (props) => { 243 | console.log('Presentational props: ' + JSON.stringify(props)); 244 | return ( 245 |
246 |
247 | logo 248 |

Welcome to React/Redux

249 |
250 | 251 |
) 252 | } 253 | 254 | export default Presentational; 255 | ``` 256 | Eu usei a sintaxe de **Função** (em vez de **Classe**) para criar o componente. Já que eu não precisarei armazenar estado interno, posso simplificar as coisas. Ficou confuso? Ele poderia ser reescrito assim: 257 | ``` 258 | function Presentational(props) { 259 | ... 260 | } 261 | ``` 262 | O que há de diferente nesse Componente? Ah, sim! Eu "puxei" o *event handler* **onClick** para o componente **Botao**. Isso é para associá-lo à função que o **mapDispatchToProps** do [**Container.js**](./src/Container.js) passar para ele. Eu recebo os **dados** e o **clickBotao** dentro das **props** e associo aos componentes de apresentação filhos. 263 | 264 | Eu também tive que alterar o componente[**Botao.js**](./src/Botao.js) para transformá-lo em **Function**. Isso facilita muito para receber o tratamento de eventos e os dados. Note que ambos, **Presentational** e **Botao** não estão cientes do **Redux**. 265 | 266 | Eis o código do componente: 267 | ``` 268 | import React, { Component } from 'react'; 269 | import Resultado from './Resultado'; 270 | 271 | const Botao = ({dados,click}) => ( 272 |
273 | 276 | {dados != null && 277 | 278 | } 279 |
280 |
281 | ); 282 | 283 | export default Botao; 284 | ``` 285 | 286 | ### Fluxo 287 | 288 | Quando a página é renderizada, o **Reducer** é invocado com o **Estado inicial**. Então, o componente **Container** é renderizado e retorna um componente **Presentational** ligado à nossa **Store**. Como a propriedade **dados** do **Estado** é nula, o componente **Resultado** não é renderizado, conforme podemos ver no código do componente **Botao**. 289 | 290 | Ao clicarmos no **Botao** o **event handler onClick** é invocado. O tratamento está no componente **Container**: 291 | ``` 292 | const mapDispatchToProps = dispatch => ({ 293 | clickBotao: () => dispatch(fetchIp()) 294 | }) 295 | ``` 296 | A ação **thunk** "fetchIp()" é invocada,e fará duas alterações no **Estado**: REQUEST_IP e depois RECEIVE_IP. 297 | 298 | Ao processar a RECEIVE_IP, o retorno da função "receiveIp()" será um novo **Estado**, que será armazenado na **Store**, de onde o **Container** pega os **dados** e mapeia às **props** do componente **Presentational**. 299 | 300 | Confuso? Estude este fluxo novamente para entender. 301 | 302 | ### Modificações no teste 303 | 304 | Eu tive que fazer algumas modificações no script de teste [**App.test.js**](./src/App.test.js) para torná-lo compatível com o *Middleware* **redux-thunk**. 305 | 306 | Tive que acrecentar os mesmos imports que eu tinha no [**index.js**](./src/index.js): 307 | ``` 308 | import { Provider } from 'react-redux'; 309 | import thunk from 'redux-thunk'; 310 | import { createStore, applyMiddleware } from 'redux'; 311 | import atualizarDados from './Reducer'; 312 | ``` 313 | Tive que criar a **Store** e aplicar o *Middleware* **thunk** no primeiro teste: 314 | ``` 315 | const store = createStore(atualizarDados, 316 | applyMiddleware(thunk)); 317 | 318 | const div = document.createElement('div'); 319 | ReactDOM.render( 320 | 321 | 322 | 323 | , div); 324 | ``` 325 | 326 | ### Conclusão 327 | 328 | Este exemplo é muito simples para demonstrar o potencial do Redux e, na verdade, nem seria necessário utilizar Redux com ele, mas creio que serve para demonstrar bem o seu uso. 329 | 330 | 331 | -------------------------------------------------------------------------------- /react_redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactredux", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "cross-fetch": "^2.1.0", 7 | "react": "^16.2.0", 8 | "react-dom": "^16.2.0", 9 | "react-redux": "^5.0.5", 10 | "react-scripts": "1.1.1", 11 | "redux": "^3.5.2", 12 | "redux-thunk": "^2.2.0" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject" 19 | }, 20 | "devDependencies": { 21 | "chai": "^4.1.2", 22 | "enzyme": "^3.3.0", 23 | "enzyme-adapter-react-16": "^1.1.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /react_redux/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/react_redux/public/favicon.ico -------------------------------------------------------------------------------- /react_redux/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /react_redux/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /react_redux/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/react_redux/react.png -------------------------------------------------------------------------------- /react_redux/react_redux_f1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/react_redux/react_redux_f1.png -------------------------------------------------------------------------------- /react_redux/redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/react_redux/redux.png -------------------------------------------------------------------------------- /react_redux/src/Actions.js: -------------------------------------------------------------------------------- 1 | import fetch from 'cross-fetch'; 2 | export const REQUEST_IP = 'REQUEST_IP'; 3 | export const RECEIVE_IP = 'RECEIVE_IP'; 4 | 5 | // Action creators: 6 | 7 | function requestIp() { 8 | return { 9 | type: REQUEST_IP 10 | } 11 | } 12 | 13 | function receiveIp(rjson) { 14 | return { 15 | type: RECEIVE_IP, 16 | dados: rjson 17 | } 18 | } 19 | 20 | // Thunk action: 21 | 22 | const url = 'http://ipinfo.io/json'; 23 | //const url = 'http://localhost:5000'; 24 | 25 | export function fetchIp() { 26 | return function (dispatch) { 27 | console.log('invoked!!!') 28 | dispatch(requestIp()) 29 | console.log('fetching...') 30 | return fetch(url) 31 | .then( 32 | response => response.json(), 33 | error => console.log('An error occurred.', error) 34 | ) 35 | .then(json => { 36 | console.log('again! ' + JSON.stringify(json)) 37 | dispatch(receiveIp(json)) 38 | } 39 | ) 40 | } 41 | } -------------------------------------------------------------------------------- /react_redux/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | -------------------------------------------------------------------------------- /react_redux/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | import Container from './Container'; 5 | 6 | class App extends Component { 7 | render() { 8 | return ( 9 | 10 | ); 11 | } 12 | } 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /react_redux/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Container from './Container'; 4 | import Botao from './Botao' 5 | import ReactTestUtils from 'react-dom/test-utils'; 6 | import { mount } from 'enzyme'; 7 | import Adapter from 'enzyme-adapter-react-16'; 8 | import { configure } from 'enzyme'; 9 | import { expect } from 'chai'; 10 | import { Provider } from 'react-redux'; 11 | import thunk from 'redux-thunk'; 12 | import { createStore, applyMiddleware } from 'redux'; 13 | import atualizarDados from './Reducer'; 14 | import App from './App'; 15 | 16 | beforeEach(function() { 17 | global.fetch = jest.fn().mockImplementation(()=> { 18 | var p = new Promise((resolve,reject) => { 19 | const response = function() {return {"ip": "192.168.1.1","hostname": "No Hostname","city": "Rio de Janeiro","region": "Rio de Janeiro","country": "BR","loc": "-22.9876,-43.3207","org": "VIVO"}; } 20 | resolve({'json':response}); 21 | }) 22 | return p; 23 | } 24 | ) 25 | 26 | }) 27 | 28 | describe('Teste da interface', () => { 29 | it('deve aparecer o botão Pesquisar, sem erros', () => { 30 | // Sem enzyme: 31 | const store = createStore(atualizarDados, 32 | applyMiddleware(thunk)); 33 | 34 | const div = document.createElement('div'); 35 | ReactDOM.render( 36 | 37 | 38 | 39 | , div); 40 | const buttons = div.getElementsByTagName("button"); 41 | //console.log(buttons[0].firstChild.data); 42 | expect(buttons[0].firstChild.data).to.be.equal('Pesquisar'); 43 | ReactDOM.unmountComponentAtNode(div); 44 | }); 45 | 46 | it('Ao ser clicado, deve retornar uma tabela', () =>{ 47 | // com enzyme: 48 | configure({ adapter: new Adapter() }); 49 | const wrapper = mount( 50 | 51 | ); 52 | const botao = wrapper.find('button'); 53 | botao.simulate('click'); 54 | setTimeout(function(){ 55 | expect(wrapper.find('table')).to.have.length(1); 56 | }, 2000); 57 | }); 58 | }); -------------------------------------------------------------------------------- /react_redux/src/Botao.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Resultado from './Resultado'; 3 | 4 | const Botao = ({dados,click}) => ( 5 |
6 | 9 | {dados != null && 10 | 11 | } 12 |
13 |
14 | ); 15 | 16 | export default Botao; 17 | 18 | -------------------------------------------------------------------------------- /react_redux/src/Container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { fetchIp } from './Actions' 3 | import Presentational from './Presentational' 4 | 5 | const mapStateToProps = state => ( 6 | {dados: state.dados} 7 | ); 8 | 9 | const mapDispatchToProps = dispatch => ({ 10 | clickBotao: () => dispatch(fetchIp()) 11 | }) 12 | 13 | export default connect( 14 | mapStateToProps, 15 | mapDispatchToProps 16 | )(Presentational) 17 | -------------------------------------------------------------------------------- /react_redux/src/Presentational.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | import Botao from './Botao'; 5 | 6 | const Presentational = (props) => { 7 | console.log('Presentational props: ' + JSON.stringify(props)); 8 | return ( 9 |
10 |
11 | logo 12 |

Welcome to React/Redux

13 |
14 | 15 |
) 16 | } 17 | 18 | 19 | 20 | export default Presentational; -------------------------------------------------------------------------------- /react_redux/src/Reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_IP, 3 | RECEIVE_IP 4 | } from './Actions' 5 | 6 | const estadoInicial = { 7 | aguardando: false, 8 | dados: null 9 | }; 10 | 11 | function atualizarDados(state = estadoInicial, action) { 12 | switch(action.type) { 13 | case REQUEST_IP: 14 | return Object.assign({}, state, { 15 | aguardando: true, 16 | dados: null 17 | } 18 | ) 19 | case RECEIVE_IP: 20 | return Object.assign({}, state, { 21 | aguardando: false, 22 | dados: action.dados 23 | }) 24 | default: 25 | return state; 26 | } 27 | } 28 | 29 | export default atualizarDados; 30 | -------------------------------------------------------------------------------- /react_redux/src/Resultado.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Resultado extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = {'dados' : props.dados} 7 | } 8 | 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
IP{this.state.dados.ip}
Hostname{this.state.dados.hostname}
Cidade{this.state.dados.city}
Região{this.state.dados.region}
País{this.state.dados.country}
Loc{this.state.dados.loc}
Organização{this.state.dados.org}
22 | ) 23 | } 24 | } 25 | 26 | export default Resultado; -------------------------------------------------------------------------------- /react_redux/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /react_redux/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createStore, applyMiddleware } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import App from './App'; 6 | import atualizarDados from './Reducer'; 7 | import thunk from 'redux-thunk'; 8 | 9 | const store = createStore(atualizarDados, 10 | applyMiddleware(thunk)); 11 | 12 | render( 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | 19 | -------------------------------------------------------------------------------- /react_redux/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /react_redux/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /redux_form/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /redux_form/README.md: -------------------------------------------------------------------------------- 1 | ![](./react.png) 2 | # React! Don't Panic! 3 | (c) 2018 [**Cleuton Sampaio**](https://github.com/cleuton). 4 | 5 | [![](../banner_livros2.png)](https://www.lcm.com.br/site/#livros/busca?term=cleuton) 6 | 7 | ![](./rf.png) 8 | 9 | ## Strike five: Formulários com Redux-form! 10 | 11 | Fala sério! Você ficou aí, coçando a cabeça pensando nisso, não? *"Como é que eu vou fazer um form?"* Ae, você não está só! Eu também fiquei meio perdido, no meio dessa **selva** de frameworks, padrões, componentes etc. 12 | 13 | É aí que entra o **Redux-form**! Existem outras soluções para tratamento de forms com React, como o [**Flux-form**](https://github.com/goatslacker/flux-form), porém, já que eu mostrei o **Redux** para gerenciamento de estado, faz todo sentido mostrar o **Redux-form**, não? Muita gente o utiliza com sucesso. 14 | 15 | ## Vamos lá... 16 | 17 | A aplicação que vou mostrar é muuuuiiiiito simples. Por que? Bem, esses frameworks já são complicados o suficiente, logo, temos que "comer o elefante pelas beiradas". 18 | 19 | É uma SPA (Single Page Application) que possui dois "estados": Antes do login, quando apresenta o form para o usuário se autenticar: 20 | 21 | ![](./redux_form_f1.png) 22 | 23 | E depois do login, quando apresenta o nome do usuário logado: 24 | 25 | ![](./redux_form_f2.png) 26 | 27 | A decisão se vai mostrar o **form** ou o **nome do usuário** é tomada no componente [**Presentational.js**](./src/Presentational.js): 28 | ``` 29 |
30 | { 31 | props.dados != null && 32 |

Usuário: {props.dados.user}

33 | } 34 | { 35 | props.dados == null && 36 | 37 | } 38 |
39 | ``` 40 | 41 | Crie uma aplicação nova com o **create-react-app**: 42 | ``` 43 | npx create-react-app redux_form 44 | cd redux_form 45 | ``` 46 | Vamos criar uma app simples, com um form de **login**. E vamos usar um servidor **python** utilizando o framework **Flask** para criar um **Microsserviço** para processar o request! Crie aí o código do servidor, que é bem simples: 47 | ``` 48 | from flask import Flask 49 | from flask_cors import CORS 50 | from flask import request 51 | app = Flask(__name__) 52 | CORS(app) 53 | @app.route('/', methods=['POST']) 54 | def logon(): 55 | return '{"user": "' + request.form['usuario'] + '","status":"autenticado"}'; 56 | ``` 57 | Salve essa *poronga* como **authserver.py**. Para executar, você precisará ter o **python** 3.3 ou superior e algumas outras "cositas". Sabe instalar o **python**? Não!? 58 | 59 | :-O 60 | 61 | Bem, ninguém é perfeito! Siga esse [**meu tutorial do pythondrops.com**](https://github.com/cleuton/pythondrops/tree/master/virtual_environment). Ative o ambiente virtual e vamos embora! 62 | 63 | Bom, para subir esse servidor, você precisa também ter o **flask** instalado, então, digite o comando abaixo, dentro do seu ambiente virtual: 64 | ``` 65 | pip install Flask 66 | pip install -U flask-cors 67 | ``` 68 | E depois, suba o servidor com: 69 | ``` 70 | FLASK_APP=authserver.py flask run 71 | ``` 72 | Para testar se o servidor está funcionando, use o **curl**: 73 | ``` 74 | $ curl -F 'usuario=fulano@teste.com' -F 'senha=teste' http://localhost:5000 75 | 76 | {"user": "fulano@teste.com","status":"autenticado"} 77 | ``` 78 | Maneiro, não? Ah, não achou? então faça em **Java**! 79 | 80 | ## Agora, vamos criar a app! 81 | 82 | ### Instações posteriores 83 | 84 | Precisamos instalar o **redux**, o **react-redux** e o **redux-thunk**. Além disto, eu gosto muito de usar o **chai** e o **enzyme** para testes. Então, rode os seguintes comandos na pasta do projeto: 85 | ``` 86 | npm install --save redux react-redux redux-thunk redux-form cross-fetch 87 | npm install --save-dev chai enzyme enzyme-adapter-react-16 88 | ``` 89 | ### Actions 90 | 91 | Bom, como temos um request assíncrono, vamos seguir aquela "receita de bolo" que fizemos no [**tutorial anterior**](../react_redux). Crie um arquivo **Actions.js** assim: 92 | ``` 93 | import fetch from 'cross-fetch'; 94 | export const REQUEST_LOGIN = 'REQUEST_LOGIN'; 95 | export const RECEIVE_LOGIN = 'RECEIVE_LOGIN'; 96 | 97 | // Action creators: 98 | 99 | function requestLogin() { 100 | return { 101 | type: REQUEST_LOGIN 102 | } 103 | } 104 | 105 | function receiveLogin(rjson) { 106 | return { 107 | type: RECEIVE_LOGIN, 108 | dados: rjson 109 | } 110 | } 111 | 112 | // Thunk action: 113 | 114 | const url = 'http://localhost:5000'; 115 | 116 | export function fetchLogin(usuario,senha) { 117 | return function (dispatch) { 118 | console.log('invoked!!!') 119 | dispatch(requestLogin()) 120 | console.log('fetching...'+usuario+','+senha) 121 | var form = []; 122 | form.push('usuario='+usuario) 123 | form.push('senha='+senha) 124 | return fetch(url,{ 125 | 'method': 'POST', 126 | 'headers': { 127 | 'Accept': 'application/json, application/xml, text/play, text/html, *.*', 128 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' 129 | }, 130 | 'body': form.join('&') 131 | }) 132 | .then( 133 | response => response.json(), 134 | error => console.log('An error occurred.', error) 135 | ) 136 | .then(json => { 137 | console.log('again! ' + JSON.stringify(json)) 138 | dispatch(receiveLogin(json)) 139 | } 140 | ) 141 | } 142 | } 143 | ``` 144 | 145 | Temos uma **thunk-action** para efetuar o login. E, por falar nisso, precisamos instalar o *Middleware*! Vamos fazer isso quando criarmos nossa **store**. 146 | 147 | ### Reducer 148 | 149 | O nosso reducer é praticamente a mesma coisa, já que a maioria do código é **boilerplate**. O **State** é composto por uma propriedade **aguardando**, que podemos usar para exibir uma animação, e uma propriedade **dados**, que receberá o JSON resultante do request. 150 | 151 | Veja o arquivo [**Reducer.js**](./src/Reducer.js). 152 | 153 | ### Store 154 | 155 | Agora é que ficará interessante! Vamos alterar o arquivo [**index.js**](./src/index.js) para criar nossa **Store** e nosso componente **Container**. 156 | ``` 157 | import React from 'react'; 158 | import { render } from 'react-dom'; 159 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 160 | import { reducer as formReducer } from 'redux-form'; 161 | import { Provider } from 'react-redux'; 162 | import App from './App'; 163 | import atualizarDados from './Reducer'; 164 | import thunk from 'redux-thunk'; 165 | 166 | const rootReducer = combineReducers({ 167 | estado: atualizarDados, 168 | form: formReducer 169 | }); 170 | 171 | const store = createStore(rootReducer, 172 | applyMiddleware(thunk)); 173 | 174 | render( 175 | 176 | 177 | , 178 | document.getElementById('root') 179 | ); 180 | 181 | 182 | ``` 183 | Para usar o **Redux-form** preciso usar o reducer especializado dele: *formReducer*. Só que eu também tenho meu próprio estado, além do estado do form, logo, tenho meu próprio reducer. 184 | 185 | Por isto, eu usei o método **combineReducers()** para criar um **rootReducer**, que combina o meu próprio reducer e o formReducer, do **Redux-form**. 186 | 187 | O meu reducer [**Reducer.js**](./src/Reducer.js) é praticamente igual ao do exempo anterior, logo, nem vou desperdiçar seu tempo falando dele. 188 | 189 | ### Componentes 190 | 191 | Eu tenho um componente superior [**App.js**](./src/App.js) que simplesmente renderiza o meu componente container: 192 | ``` 193 | class App extends Component { 194 | render() { 195 | return ( 196 | 197 | ); 198 | } 199 | } 200 | ``` 201 | O componente container, cujo nome é [**Container.js**](./src/Container.js) serve para conectar o Redux com os outros componentes de apresentação. Ele se conecta à **Store** e usa a função **connect()** para obter o estado e repassar aos componentes de apresentação como **props**: 202 | ``` 203 | const mapStateToProps = state => { 204 | return {dados: state.estado.dados} 205 | }; 206 | 207 | const mapDispatchToProps = dispatch => ({ 208 | procsubmit: (values) => { 209 | console.log("Dispatch: " + values) 210 | dispatch(fetchLogin(values.usuario,values.senha)) 211 | } 212 | }) 213 | 214 | export default connect(mapStateToProps, mapDispatchToProps)(Presentational); 215 | ``` 216 | 217 | Vamos falar um pouco sobre o **mapDispatchToProps**. Ele serve para mapear o disparo de **Actions** do Redux. Eu criei uma função **procsubmit()** que recebe um objeto vindo do evento **onSubmit** do meu **Form**. Através deste objeto, eu tenho acesso aos valores dos campos do formulário, e posso disparar a ação **fetchLogin()**, que é uma **Thunk action**. Eu passo o que o usuário digitou (usuário e senha). 218 | 219 | Para organizar os componentes de apresentação, criei um componente organizador chamado: [**Presentational.js**](./src/Presentational.js): 220 | ``` 221 | const Presentational = (props) => { 222 | const { procsubmit } = props 223 | const submit = values => {procsubmit(values)} 224 | console.log("Pres:"+JSON.stringify(props.dados)) 225 | return ( 226 |
227 | { 228 | props.dados != null && 229 |

Usuário: {props.dados.user}

230 | } 231 | { 232 | props.dados == null && 233 | 234 | } 235 |
) 236 | } 237 | 238 | export default Presentational; 239 | ``` 240 | O que ele faz? Bem, ele renderiza o form ou o nome do usuário, dependendo do estado da aplicação. Ele não tem consciência do Redux, logo, recebe o estado como **props**. Ele recebe a função que eu dispachei com o **mapDispatchToProps** e associa a um tratador de evento local, passando isto para o componente do formulário. 241 | 242 | Um comando interessante é este: 243 | ``` 244 | const { procsubmit } = props 245 | ``` 246 | Ele é do ES6 um [**Desestructuring assignment**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) que procura uma propriedade com o mesmo nome dentro de **props** e a associa a este objeto. 247 | 248 | Ok, finalmente, tenho o componente de formulário: [**LoginForm.js**](./src/LoginForm.js): 249 | ``` 250 | let LoginForm = props => { 251 | const { handleSubmit } = props 252 | return ( 253 |
254 |
255 |
256 |
257 | 258 | 259 |
260 |
261 | 262 | 263 |
264 | 265 |
266 |
267 |
268 | ) 269 | } 270 | 271 | export default reduxForm({ 272 | form: 'login' 273 | })(LoginForm) 274 | ``` 275 | Ele recebe o handler da função de tratamento de submit, oriundo do **Presentational** e repassa no evento **onSubmit** do form. E ele "empacota" os componentes de form dentro de componentes **Field**. 276 | 277 | Por fim, temos que encapsular o LoginForm, decorando-o com a função **reduxForm()**. Esta função, assim como a **conect()** são **HOCs** ou [**High Order Components**](https://reactjs.org/docs/higher-order-components.html), que "decoram" ou "encapsulam" componentes, retornando novos componentes. 278 | 279 | Todos os eventos de form (onFocus, onBlur etc) são tratados pelo Redux-form através desse HOC ("reduxForm()"). 280 | 281 | ### Combinando estado de app com estado do form 282 | 283 | Naturalmente, você possui estado da app, como o nome do usuário logado, por exemplo, e estado que é local ao form (conteúdo de campos). Cada **reducer** trata de um estado específico. O mais importante é separar em componentes **Container** e componentes de **Apresentação**, sendo que, estes últimos, recebem o estado somente através de **props**. 284 | 285 | Só para terminar, desta vez eu não criei um teste detalhado, mas você pode aproveitar o arquivo [**App.test.js**](./src/App.test.js) e fazer isto! Veja o tutorial anterior. 286 | -------------------------------------------------------------------------------- /redux_form/__pycache__/authserver.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/redux_form/__pycache__/authserver.cpython-36.pyc -------------------------------------------------------------------------------- /redux_form/authserver.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_cors import CORS 3 | from flask import request 4 | app = Flask(__name__) 5 | CORS(app) 6 | @app.route('/', methods=['POST']) 7 | def logon(): 8 | return '{"user": "' + request.form['usuario'] + '","status":"autenticado"}'; -------------------------------------------------------------------------------- /redux_form/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux_form", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "cross-fetch": "^2.1.0", 7 | "react": "^16.3.1", 8 | "react-dom": "^16.3.1", 9 | "react-redux": "^5.0.7", 10 | "react-scripts": "1.1.4", 11 | "recompose": "^0.26.0", 12 | "redux": "^3.7.2", 13 | "redux-form": "^7.3.0", 14 | "redux-thunk": "^2.2.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "devDependencies": { 23 | "chai": "^4.1.2", 24 | "enzyme": "^3.3.0", 25 | "enzyme-adapter-react-16": "^1.1.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /redux_form/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/redux_form/public/favicon.ico -------------------------------------------------------------------------------- /redux_form/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /redux_form/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /redux_form/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/redux_form/react.png -------------------------------------------------------------------------------- /redux_form/redux_form.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/redux_form/redux_form.tar -------------------------------------------------------------------------------- /redux_form/redux_form_f1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/redux_form/redux_form_f1.png -------------------------------------------------------------------------------- /redux_form/redux_form_f2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/redux_form/redux_form_f2.png -------------------------------------------------------------------------------- /redux_form/rf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleuton/ReactDontPanic/06a1acceb4e2eeafeae357e37f05a0cfe927d43a/redux_form/rf.png -------------------------------------------------------------------------------- /redux_form/src/Actions.js: -------------------------------------------------------------------------------- 1 | import fetch from 'cross-fetch'; 2 | export const REQUEST_LOGIN = 'REQUEST_LOGIN'; 3 | export const RECEIVE_LOGIN = 'RECEIVE_LOGIN'; 4 | 5 | // Action creators: 6 | 7 | function requestLogin() { 8 | return { 9 | type: REQUEST_LOGIN 10 | } 11 | } 12 | 13 | function receiveLogin(rjson) { 14 | return { 15 | type: RECEIVE_LOGIN, 16 | dados: rjson 17 | } 18 | } 19 | 20 | // Thunk action: 21 | 22 | const url = 'http://localhost:5000'; 23 | 24 | export function fetchLogin(usuario,senha) { 25 | return function (dispatch) { 26 | console.log('invoked!!!') 27 | dispatch(requestLogin()) 28 | console.log('fetching...'+usuario+','+senha) 29 | var form = []; 30 | form.push('usuario='+usuario) 31 | form.push('senha='+senha) 32 | return fetch(url,{ 33 | 'method': 'POST', 34 | 'headers': { 35 | 'Accept': 'application/json, application/xml, text/play, text/html, *.*', 36 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' 37 | }, 38 | 'body': form.join('&') 39 | }) 40 | .then( 41 | response => response.json(), 42 | error => console.log('An error occurred.', error) 43 | ) 44 | .then(json => { 45 | console.log('again! ' + JSON.stringify(json)) 46 | dispatch(receiveLogin(json)) 47 | } 48 | ) 49 | } 50 | } -------------------------------------------------------------------------------- /redux_form/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | -------------------------------------------------------------------------------- /redux_form/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | import Container from './Container'; 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 | 9 | ); 10 | } 11 | } 12 | 13 | export default App; -------------------------------------------------------------------------------- /redux_form/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /redux_form/src/Container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { fetchLogin } from './Actions' 3 | import Presentational from './Presentational' 4 | 5 | const mapStateToProps = state => { 6 | return {dados: state.estado.dados} 7 | }; 8 | 9 | const mapDispatchToProps = dispatch => ({ 10 | procsubmit: (values) => { 11 | console.log("Dispatch: " + values) 12 | dispatch(fetchLogin(values.usuario,values.senha)) 13 | } 14 | }) 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(Presentational); 17 | -------------------------------------------------------------------------------- /redux_form/src/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Field, reduxForm } from 'redux-form' 3 | 4 | let LoginForm = props => { 5 | const { handleSubmit } = props 6 | return ( 7 |
8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |
20 |
21 |
22 | ) 23 | } 24 | 25 | export default reduxForm({ 26 | form: 'login' 27 | })(LoginForm) 28 | -------------------------------------------------------------------------------- /redux_form/src/Presentational.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import LoginForm from './LoginForm' 4 | 5 | const Presentational = (props) => { 6 | const { procsubmit } = props 7 | const submit = values => {procsubmit(values)} 8 | console.log("Pres:"+JSON.stringify(props.dados)) 9 | return ( 10 |
11 | { 12 | props.dados != null && 13 |

Usuário: {props.dados.user}

14 | } 15 | { 16 | props.dados == null && 17 | 18 | } 19 |
) 20 | } 21 | 22 | export default Presentational; -------------------------------------------------------------------------------- /redux_form/src/Reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_LOGIN, 3 | RECEIVE_LOGIN 4 | } from './Actions' 5 | 6 | const estadoInicial = { 7 | aguardando: false, 8 | dados: null 9 | }; 10 | 11 | function atualizarDados(state = estadoInicial, action) { 12 | switch(action.type) { 13 | case REQUEST_LOGIN: 14 | return Object.assign({}, state, { 15 | aguardando: true, 16 | dados: null 17 | } 18 | ) 19 | case RECEIVE_LOGIN: 20 | return Object.assign({}, state, { 21 | aguardando: false, 22 | dados: action.dados 23 | }) 24 | default: 25 | return state; 26 | } 27 | } 28 | 29 | export default atualizarDados; 30 | -------------------------------------------------------------------------------- /redux_form/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /redux_form/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 4 | import { reducer as formReducer } from 'redux-form'; 5 | import { Provider } from 'react-redux'; 6 | import App from './App'; 7 | import atualizarDados from './Reducer'; 8 | import thunk from 'redux-thunk'; 9 | 10 | const rootReducer = combineReducers({ 11 | estado: atualizarDados, 12 | form: formReducer 13 | }); 14 | 15 | const store = createStore(rootReducer, 16 | applyMiddleware(thunk)); 17 | 18 | render( 19 | 20 | 21 | , 22 | document.getElementById('root') 23 | ); 24 | 25 | -------------------------------------------------------------------------------- /redux_form/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /redux_form/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------