Avec un peu (beaucoup) de JavaScript...
Aurélien David - @spyl94
Retour d'expérience sur
Startup civique experte en intelligence collective qui développe des applications participatives clé en main.
En travaillant la Developper eXperience
On watch en ~300ms
On code en ES6
On évite Bower!
Branche master utilisée en production par
L'API haut niveau < 15 fonctions
On le teste quand même sur un petit projet
 pour confirmer notre choix.
const CommentSection = React.createClass({
propTypes: {
post: PropTypes.number.isRequired, // post id, we will use it to fetch the comments
},
getInitialState() {
return {
comments: [],
isLoading: true,
};
},
componentDidMount() { // executed after first render
this.loadComments();
},
loadComments() {
// todo fetch an API
this.setState({
comments: [{body: 'commentaire 1'}, {body: 'commentaire 2'}],
isLoading: false,
});
},
render() {
return (
<div className="comments__section">
<CommentForm />
<Loader show={this.state.isLoading}>
<CommentList comments={this.state.comments} />
</Loader>
</div>
);
},
});
export default CommentSection;
// app.js
import React from 'react';
import ReactDOM from 'react-dom';
import CommentSection from './components/Comment/CommentSection';
if ($('#render-post-comments').length) {
ReactDOM.render(
<CommentSection post={$('#render-post-comments').data('post')} />,
document.getElementById('render-post-comments')
);
}
{% extends '@App/Blog/index.html.twig' %}
{% block body %}
<h1>{{ post.title }}</h1>
<div id="render-post-comments" data-post="{{ post.id }}"></div>
{% endblock %}
# features/api/post_comments.feature
Feature: Posts comments
Scenario: API client wants to list comments of a blogpost
When I send a GET request to "/api/posts/1/comments"
Then the JSON response should match:
"""
{
"comments_count": @integer@,
"comments":
[
{
"id": @integer@,
"body": @string@,
"created_at": "@[email protected]()",
"updated_at": "@[email protected]()",
"votes_count": @integer@,
"author": {
"username": @string@,
"media": @...@
},
"can_edit": @boolean@
},
@...@
]
}
"""
@security
Scenario: Anonymous API client wants to add a comment
When I send a POST request to "/api/posts/1/comments" with json:
"""
{
"body": "Vive les SfPots !"
}
"""
Then the JSON response status code should be 400
@database
Scenario: logged in API client wants to add a comment
Given I am logged in to api as user
When I send a POST request to "/api/posts/1/comments" with json:
"""
{
"body": "Vive les SfPots ! c'est encore mieux en étant connecté :)"
}
"""
Then the JSON response status code should be 201
@security
Scenario: logged in API client wants to add an empty comment
Given I am logged in to api as user
When I send a POST request to "/api/posts/1/comments" with json:
"""
{
"body": ""
}
"""
Then the JSON response status code should be 400
/**
* @Post("/posts/{id}/comments")
* @ParamConverter("post", options={"mapping": {"id": "id"}})
* @View(statusCode=201, serializerGroups={"Comments", "UsersInfos"})
*/
public function postPostCommentsAction(Request $request, BlogPost $post)
{
$comment = (new PostComment())
->setAuthor($this->getUser())
->setPost($post);
$form = $this->createForm(new CommentForm(), $comment);
$form->handleRequest($request);
if (!$form->isValid()) {
return $form;
}
$this->getDoctrine()->getManager()->persist($comment);
$this->getDoctrine()->getManager()->flush();
$this->get('event_dispatcher')->dispatch(
AppBundleEvents::COMMENT_ADDED,
new CommentAddedEvent($comment)
);
}
const status = (response) => {
if (response.status >= 200 && response.status < 300) {
return response;
}
throw new Error(response.statusText);
};
const json = (response) => response ? response.json() : {};
const getHeaders = () => AuthService.isJwtValid()
? { Authorization: 'Bearer ' + AuthService.getJwt() }
: {};
class Fetcher {
get(uri) {
return AuthService.login().then(() => {
return fetch(config.api + uri, {
method: 'get',
headers: getHeaders(),
}).then(status).then(json);
});
}
post(uri, body) {
return AuthService.login().then(() => {
return fetch(config.api + uri, {
method: 'post',
headers: getHeaders(),
body: JSON.stringify(body),
}).then(status);
});
}
}
const CommentSection = React.createClass({
// ...
loadComments() {
Fetcher
.get(`posts/${this.props.post}/comments`)
.then((data) => {
this.setState({
comments: data.comments,
isLoading: false,
});
})
;
},
});
const CommentForm = React.createClass({
// ...
handleSubmit() {
Fetcher
.post(`posts/${this.props.post}/comments`, this.state.form)
.then(() => {
this.setState(this.getInitialState());
})
;
}
});
Paginer, revenir en arrière, communiquer d'un composant enfant vers un composant parent....
On se retrouve avec une dizaine d'applications React réparties sur le site et plus de 150 composants.
Â
Â
// MyComponent-test.js
import React from 'react';
import { shallow } from 'enzyme';
import { expect } from 'chai';
describe('<MyComponent />', () => {
it('renders three <Foo /> components', () => {
const wrapper = shallow(<MyComponent />);
expect(wrapper.find('Foo')).to.have.length(3);
});
});
Tests unitaires
// .eslintrc
{
"extends": "jolicode",
"plugins": [
"react"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"react/prefer-es6-class": 0
}
}
Linter
Au fait le SEO ?
Et les pages blanches quand t'exécutes pas le JS ? (oui, ça existe encore, la preuve au prochain slide)
Quelqu'un a utilisé le site republique-numerique.fr en utilisant mon nom [...]
En vérité, je ne peux pas utiliser ce site car j'en suis techniquement exclu.
[...]
 Le texte des articles n'apparaît pas dans mon navigateur : ce site ne fonctionne pas dans mon cas.
Il requiert l'exécution de programmes privateurs en JavaScript.
[...]
Mais la meilleure solution consiste, sans aucun doute, Ã faire fonctionner le site sans code JavaScript.
Â
Richard Stallman
https://www.april.org/republique-numeriquefr-de-lusurpation-didentite-la-confiscation-de-la-liberte
$ composer require limenius/react-bundle
# AppBundle:Blog:show.html.twig
{{ react_component('CommentSectionApp', {'props': commentSectionProps}) }}
public function showAction(BlogPost $post)
{
$serializer = $this->get('jms_serializer');
$commentSectionProps = $serializer->serialize([
'comments' => $post->getComments(),
], 'json', SerializationContext::create());
return [
'post' => $post,
'commentSectionProps' => $commentSectionProps,
];
}
// app.js
import './registration';
/* the rest of my JS code ... */
// registration.js
import React from 'react';
import ReactOnRails from 'react-on-rails';
import CommentSection from './components/CommentSection';
const CommentSectionApp = (props) => <CommentSection {...props} />;
ReactOnRails.register({ CommentSectionApp });
limenius_react:
serverside_rendering:
# Location of the server bundle, that contains React and React on Rails.
server_bundle_path: "%kernel.root_dir%/../web/js/server-bundle.js"
On ne passe plus forcément par une API pour afficher les données initiales, on préfère injecter le JSON en props directement.
On a réussi à rendre nos composants en html avant d'envoyer la page au client, on récupère donc les bénéfices de SEO et on évite les pages blanches pour les utilisateurs sans JS.
Côté client le JS reprend la main et s'exécute normalement, en utilisant le rendu serveur comme état initial.
C'est très couteux en performances ! Il faut absolument utiliser un reverse proxy.
En ajoutant un feature flag pour désactiver le rendu serveur, on tient 3 fois plus d'utilisateurs connectés.
{{ react_component('CommentSectionApp', {
'props': commentSectionProps,
'rendering': (app.user and not feature_is_active('server_side_rendering'))
? 'client_side'
: 'both',
})
}}
D'où l'importance de développer des composants intelligents capables de récupérer les données manquantes !
Mais des temps de développement rallongés, donc s'assurer que c'est essentiel à votre business !
Beaucoup de gains en clarté et organisation du code; facilité à tester et plaisir de coder.
Eviter la Reactite aigüe, on peut encore utiliser Twig pour un affichage simple.
Aurélien David - @spyl94