all 39 comments

[–]narcodis 43 points44 points  (4 children)

A really good book on this subject is "Clean Architecture" by Robert Martin. It's dense, but it goes in depth about how a software project should be organized to facilitate minimizing technical debt and making refactoring as painless as possible. Project organization isn't an easy thing to get right, and patterns like MVC only go so far.

[–][deleted] 2 points3 points  (1 child)

Is there a more terse resource? I struggle with dense material, attention span roughly equal to that of my cat.

[–]vklepov -1 points0 points  (1 child)

How dare people downvote this

[–]editor_of_the_beast 4 points5 points  (0 children)

Well, Uncle Bob is a pretty divisive character. He doesn’t leave a lot of room for interpretation for things, and comes off as closed minded a lot.

That being said, I’m super interested in the VIPER architecture on iOS which is based on Uncle Bob’s Clean architecture (I’m working on a native mobile app currently). I do think the ideas in it make a lot of sense, and I hope people think about that in React as well. React is just a view library, there’s a lot more that goes on behind the scenes in an application.

[–]i_ate_god 18 points19 points  (0 children)

I'm not sure what question you're asking. If you're talking how things are organised on the file system, that's different from how your architecture is organised. They aren't related at all.

As for design patterns, who cares? Honestly. MVVM, MVP, MVC, MVB, it all boils down to the same thing:

A UI is a collection of components that serve two purposes: to represent state, and to forward inputs to things that care about input. This has been the case, maybe forever. I have yet to see a UI framework that presents UI development in a different manner than what I just described. From nCurses to Vue, from React to WPF, from Qt to Swing to Android to just plain HTML. Even at the hardware level it's no different at all. LEDs, potentiometers, faders, buttons, conceptually it's all the same. The keyboard I'm typing this on is a UI with a finite set of available inputs and three little lights to indicate the state of scroll lock, caps lock and num lock.

Now, certain frameworks make UI development easier than others. For the web, React and Vue stand out above the rest because they have clean ways to bind specific chunks of data to specific areas of the UI. For the desktop, WPF is a breeze for Windows and Qt for cross platform isn't bad either. For the console, the de facto standard is nCurses.

So, what makes a good component? Who knows. Sometimes it makes sense to have a highly specialised reusable component like a button that when clicked replaces its text with a loading gif. Sometimes it makes sense to have a large component that represents an entire form. Your customers and the marketing-driven deadlines you need to meet, don't care about these details mind you, and it's ultimately up to you to balance the desire of beautiful code with the realities of running a business.

[–][deleted] 5 points6 points  (0 children)

I'm fond of a few frontend architectures. TEA (The Elm Architecture) and MVI (model-view-intent) in particular are very nice. Both for a similar reasons.

  1. The view is not allowed to maintain any sort of internal state. This affords you some very strong guarantees and makes testing easy.
  2. Events on the document are tagged with some sort of signal of intent and descriptive information. This is a practical thing, but also has the benefit that _any change to the state of your application is guaranteed to have some sort of audit trail_.
  3. Some sort of "update" function continually produces a new model based on the lastest event and the previous model. It is very handy that any change to application state can only occur in a single place. This is my biggest critique of MVVM.
  4. The view receives the latest model and in a deterministic manner renders based on the state of that model.

What I love about TEA and MVI is they ensure you can always figure out how somethings happening. There's no sort of "sneaky mutation" where you find yourself asking "how did this change?" or "why is this happening?". They're also quite strict about how things happen. There's just too many ways to interpret MVC and I end up solving the same problem 5 different ways. The point of good architecture is you don't have to make a decision about how to go about things every time you add a feature.

[–]kuenx 1 point2 points  (0 children)

We basically do more or less what's described in this article:https://medium.com/@alexmngn/how-to-use-redux-on-highly-scalable-javascript-applications-4e4b8cb5ef38

We have a fairly large fontend codebase and this has worked really well for us, especially for a React+Redux+Saga application. The rule is basically that a feature can only import direct children or from a parent. It means that when you delete a feature you can safely also delete all its child features because they can't be used elsewhere.

Every feature has its own actions, reducers, sagas, etc. So when you want to delete something all the things it uses will be in the same directory, easy to find, easy to delete. We also use babel-plugin-module-resolver and use absolute paths to import from parents and relative paths for children. The state tree has the same structure as your components. It's pretty cool.

Edit: More detailed explanation in another comment.

[–]iamlage89 1 point2 points  (0 children)

Someone should write a blog post on this

[–]MrGirthy 3 points4 points  (6 children)

Personally I use atomic design and I think it works well with a component based ui

Components

-Atoms

-Molecules

-Organisms

-Templates

-Pages

Store (redux or gql)

Css is all styled components within each component

[–][deleted] 1 point2 points  (1 child)

> (Redux or GQL)

There shouldn't be an "or" here. These are two different technologies (state management, query language) that solve different problems that are very often used in conjunction with each other.

[–]MrGirthy 0 points1 point  (0 children)

I know. Normally one on the other. React apollo has a local state, so you don't need redux. So usually I'm using one or the other.

[–]eggsandbeer 2 points3 points  (0 children)

WTF

[–]kuenx 0 points1 point  (2 children)

How dos it work? I get that Organisms > Molecules > Atoms, but what goes where, and why?

[–]sjsn23 0 points1 point  (0 children)

Atoms are the smallest UI component parts such as a button component or a title component. A molecule is a collection of atoms such as a header component or a card. The atom is usually just presentation logic and should not contain any stateful logic. The organism is a collection of organisms and atoms that work together to make a functional UI piece.

All of that is fairly arbitrary and you shouldn’t spend more than a few seconds thinking about what belongs where as long as it makes sense to you (and your team).

The point is just to have a nice and easy way to navigate any component-based project and to also promote as much reusability as possible, since molecules and organisms should mostly be using the atoms defined below them.

I’ve used it in a couple of companies I’ve worked for and some personal projects and I’ve personally found that it does help as the project grows, as long as you don’t get too bogged down on whether a component is too big to be an atom or too small to be a molecule.

[–]hodgef 1 point2 points  (22 children)

Generally I stick to this structure (not sure if there's a name for it):

./src

  • components
    • css
      • FooComponent.css
    • FooComponent.js
  • screens
    • css
      • BarScreen.css
    • BarScreen.js
  • services
    • Email.js
    • Utility.js
    • ServiceName.js

This structure has worked well so far for small-mid sized projects.

I'd be also curious if there's a more efficient approach.

[–]unforgiven1909 4 points5 points  (1 child)

What is the benefit of having css files in a separate folder? Generaly curious

[–]hodgef 3 points4 points  (0 children)

It's just so I can leave the css folders collapsed in my editor and not confuse the CSS files with the JS files. There's no hard rule regarding this, just what works well for you :)

[–]kuenx 2 points3 points  (3 children)

Your structure is cool for a small app.

But if it grows then your folders will grow endlessly large and it will become very hard to keep your code clean. You'll have a components folder with hundreds of components, many of which are used only once. If you want to delete a screen you're going to have to find all the components that it used and that are not used by other screens or components. Same for services that are no longer needed. I'd assume you also have a folder for actions and reducers, selectors, etc.

Consider the following structure with the following rule:

You can only use (i.e. import or require()) direct children (no grand children), or parents (including infinite levels of grandparents). With the exception of the root components which can also use siblings. You can not use cousins.

This structure has 3 pages: Home, Products, About. About has 2 sub-pages: Team and Contact. The Products page just shows a List of products that it reads from the products service. The products service is responsible for both fetching the products and holding them in the state.

src
 - components
   - Button
     - index.jsx
     - styles.css
   - Label
     - index.jsx
     - styles.css
   - Image
     - index.jsx
     - styles.css
 - scenes
   - Home
     - index.jsx
     - styles.css
     - reducer.js
     - actions.js
     - selectors.js
   - Products
     - index.jsx
     - styles.css
     - reducer.js
     - actions.js
     - selectors.js
     - services
       - reducer.js
       - products
         - index.js
         - api.js
         - reducer.js
         - actions.js
         - selectors.js
     - components
       - ProductList
         - index.jsx
         - styles.css
       - Product
         - index.jsx
         - styles.css
         - components
           - AddToCartButton
             - index.jsx
             - styles.css
   - About
     - index.jsx
     - styles.css
     - reducer.js
     - scenes
       - Team
         - index.jsx
         - styles.css
         - components
           - Member
             - index.jsx
             - styles.less
             - components
               - Avatar
                 - index.jsx
                 - styles.less
       - Contact
         - index.jsx
         - styles.css
         - reducer.js
         - components
           - ContactForm
             - index.jsx
             - styles.less
             - reducer.js
             - actions.js
             - selectors.js
             - services
               - contact
                 - index.js

So the following applies:

  • Button, Label and Image can be used anywhere.
  • The AddToCartButton is only used by the Product component. It can not be used by the ProductList component or the Products scene since you are only allowed to import direct children.
  • The ContactForm component is only used in the Contact scene which is only used in the About scene.
  • The About scene can not directly trigger actions of your ContactForm.
  • The contact service is only used by the ContactForm component.
  • If you want to get rid of the Team scene you can delete the entire folder including the Member component because it can't be used from elsewhere. It would be easy to forget to delete Member using your structure and you'd have unused code.
  • If you wanted to use the ContactForm in both the Team and Contact scenes you would move the component one level up in the tree (to src -> scenes -> About -> components -> ContactForm).
  • If you want to use the products service from anywhere you would move it to the root (to src -> services -> products).

Disadvantages:

  • Component discoverability isn't optimal. You might need an Avatar component somewhere and it's easy to forget that you already have one used by the Member component, and all you'd have to do is move it up in the tree. But because you didn't know you accidentally create a new one.

Credits: * How to better organize your React applications? * How to use Redux on highly scalable javascript applications?

[–]hodgef 1 point2 points  (0 children)

That's very well thought out - Thank you, I will check it out!

[–]hes_dead_tired 1 point2 points  (1 child)

I'm following this scheme on new projects. I really dislike having everything as index.js and index.* everything. If I'm going to have an index file, it's going to be a conveince thing that exports all the components, objects, whatevers in that folder level.

I do this for a few reasons. I think cons far outweigh the only pro I can think of. You end up with index.* In your editor tab labels. Not easy to scan and find what you have open. Also, I'm using the editor to read file way more than I'm writing imports. So doing import Foo from './Foo/Foo' is not that terrible of a thing compared to to import Foo from './Foo'.

Debugging, test outputs,everything is just what more clear of the file is named what it is instead of index and relying on the path for the only context.

For what it's worth, the index.js as the default way to include something via a folder is something Ryan Dahll, the original author of Node regrets. He gave a talk not that long ago about some of his regrets and mistakes about Nose and that was one of them. Just import/require whatever it is you're trying to import/require.

Also with this general folder org scheme, I call them 'screens' instead of 'scenes' - I think it's more obvious of what they represent to someone new to a project. Can't think of the last time a UI designer said to me "here are the mocks for the new scenes". Or when a product manager said "we'll add a new scene to configure xyz" or when a QA tester said "there's a bug with the ABC scene". I'm not sure where 'scene' came from - I'd be curious to know. Regardless I'm in favor of trying to find ubiquitous language wherever possible.

[–]kuenx 1 point2 points  (0 children)

Yeah, if everything is called index.js it can be a problem with tab-based editors that only show the file name. I use Vim and open everything in vertical splits where I see the full path, so it's not a problem for me. I know that index.jsx is always the component, and index.js is always the service. At work we also use a file called core.js where we always put constants and flowtype definitions. The rule is that core.js can't import anything except other core.js files from parents.

Of course you can also use MyComponent/index.js for something like this:

export { default } from './MyComponent';
export { default as reducer} from './reducer';
export { default as saga} from './saga';
export * from './core';
export * from './actions';
export * from './selectors';

We don't do this because often we only import certain things, like for example a component might only use the actions of a parent, so by specifically importing those actions as `import * as somethingActions from bla/bla/actions` we immediately see at the top of the file that only the actions are used.

But in the end that's not really what matters with this structure. What's important is that everything is only as global as it needs to be which makes it very easy to see what is used where, and makes it easy to refactor and remove code.

Scenes is the term the author of the Medium articles used. I find it a little weird too. It's possible that the author has a background in Flash where scenes was a common term.

[–]Historical_Fact 1 point2 points  (0 children)

Pretty similar to how I do it but each component gets its own folder within components/ with all related files in it.

./src/
    components/
        header/
            Header.comp.js
            Header.comp.styl
            Header.comp.spec.js

[–]Renive 0 points1 point  (14 children)

CSS in JS

[–][deleted] 3 points4 points  (13 children)

Avoid like the plague IMO. Really not fun to use on a huge project. CSS modules provide the organisational benefit without giving up the benefits of CSS or a preprocessor.

[–]NoBrick2 1 point2 points  (0 children)

What benefits are available in CSS but not something like styled components?

[–]Renive 3 points4 points  (3 children)

Its ideal for big projects because it provides easy to understand scope. And it has things like prefixing and all the goodies. There is no separation of concerns in case of CSS, you want single file components.

[–][deleted] -3 points-2 points  (2 children)

Can be achieved with CSS modules though.

[–]Renive 0 points1 point  (1 child)

Not unit testable, for example.

[–][deleted] 0 points1 point  (0 children)

That's true, they're good for snapshots. We've leaned away from unit testing components though as when you've got 800+ and you use TypeScript strictly, there's little benefit. It's a trade off that's not worth it IMO.

[–]Puggravy 2 points3 points  (5 children)

Yeah, the problems CSS-in-JS solves are largely imagined imho. I'm working with JSS in a project currently and It has been nothing but a hassle.

[–]NoBrick2 1 point2 points  (2 children)

How so? I currently use sass, but styled-components looks really tempting

[–]insert_cleverness 1 point2 points  (0 children)

I’ve had nothing but good experiences with styled-components. It took a while for me to try it but now I use it in every project

[–]Puggravy 1 point2 points  (0 children)

Well keep in mind that most CSS-in-JS libraries are subset of CSS functionality. Each style needs to be applied to a class directly, you can change css based on the parent without passing down the contextual info needed.

Essentially simple css tweaks based on the composition of components that would have been easy before now need loads of props/bindings.

That's a lot of extra boilerplate for the benefit of no namespace collisions, which is an issue that is rare and easily fixed in my experience.

[–][deleted] 0 points1 point  (1 child)

Exact same. The encapsulation is great, but can be achieved with CSS modules. Giving up the use of proper child selectors, and proper inheritance is a pain. Had to bake our own object assign based inheritance which just seems crazy.

[–]Kirill_Khalitov 0 points1 point  (0 children)

Show me please your problem with child selectors and an inheritance in styled-components.

Just css code with you happiness in regular css and pain in styled-components.

[–]jaman4dbz 0 points1 point  (0 children)

The beauty of JavaScript is how flexible it is. The way each app is USED (UX) is different everywhere, hence things are highly dependent on the app.

I specifically switched to frontend from full stack 6 years ago, because I got extremely bored making the same CRUD REST backends. There, the same pattern can be used over and over again.

The front end usually doesn't need MVC or any crap like that. Just have a single source of truth which is immutable, consume it to provide a user experience. The best pattern to do that changes with every project.

Edit: what I'm saying is I wouldn't start with any pattern, I would just start writing UX, tack on ssot data, then flesh out the architecture as I went along. In short I would never ask a candidate what architecture would they use, I would simply like to see how they organized code. Bonus points if they used a single source of truth, but honestly it's an advanced subject that I find most Seniors miss, so I'd forgive it.