DEV Community

Fouzia Naaz
Fouzia Naaz

Posted on

How I Refactored a Messy React Native Screen into MVVM (A Real Example)

Most React Native projects don't start messy.

They become messy over time.

A screen that once handled simple UI gradually starts doing everything:

  • API calls
  • state management
  • data transformation
  • navigation logic

I recently refactored one such screen, and the difference was bigger than I expected.

The Problem: Everything in One Place

The original screen looked something like this:

  • API calls inside useEffect
  • Multiple useState hooks
  • Inline business logic
  • Conditional rendering everywhere

It worked - but it wasn't scalable.

The issues were clear:

  • Hard to read
  • Hard to test
  • Hard to reuse logic

Before: A Typical "Messy" Structure

const Screen = () => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    fetchData();
  }, []);
  const fetchData = async () => {
    setLoading(true);
    const res = await apiCall();
    const filtered = res.filter(item => item.active);
    setData(filtered);
    setLoading(false);
  };
  return (
    <>
      {loading ? <Loader /> : data.map(item => <Item key={item.id} />)}
    </>
  );
};

Enter fullscreen mode Exit fullscreen mode

Everything is tightly coupled.

The Goal
I wanted to:

  • Separate UI from logic
  • Make the code reusable
  • Improve readability
  • Prepare for scaling

After: Introducing MVVM

I split the logic into:

  • View (UI)
  • ViewModel (logic)
  • Service (API layer)

ViewModel (logic layer)

export const useScreenViewModel = () => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);

  const fetchData = async () => {
    setLoading(true);
    const res = await apiCall();
    const filtered = res.filter(item => item.active);
    setData(filtered);
    setLoading(false);
  };
  return {
    data,
    loading,
    fetchData,
  };
};
Enter fullscreen mode Exit fullscreen mode

View (clean UI)

const Screen = () => {
  const { data, loading, fetchData } = useScreenViewModel();

  useEffect(() => {
    fetchData();
  }, []);
  if (loading) return <Loader />;
  return data.map(item => <Item key={item.id} />);
};
Enter fullscreen mode Exit fullscreen mode

What Changed

  1. Cleaner Components UI is now focused only on rendering.
  2. Reusable Logic ViewModel can be reused or tested independently.
  3. Easier Debugging Logic is centralized and predictable.
  4. Better Team Collaboration Different developers can work on UI and logic separately.

What Didn't Change

The functionality stayed exactly the same.
This is important:
Refactoring is about improving structure - not changing behavior.

Lessons Learned

  • Messy code is usually a result of growth, not bad intentions
  • Small architectural decisions compound over time
  • Separation of concerns is not optional in scaling apps

When to Use This Approach

You don't need MVVM for every screen.
Use it when:

  • Logic starts growing
  • State becomes complex
  • Multiple developers are involved

Final Thoughts

React Native gives you flexibility but that flexibility can become chaos if you don't define structure.
Refactoring this screen reminded me of one thing:
Clean architecture is not about perfection it's about making future changes easier.

Top comments (0)