React for Vue devs

Components

<!-- Counter.vue -->

<template>
  <button>{{ count }}</button>
</template>

<script setup>
const count = 0;
</script>
// Counter.jsx

export default function Counter() {
  const count = 0;
  
  return <button>{count}</button>
}

Props

<!-- Counter.vue -->

<template>
  <button>
    {{ count }}
  </button>
</template>

<script setup lang="ts">
type Props = {
  count: number;
}

const props = defineProps<Props>();
</script>
// Counter.tsx

type Props = {
  count: number;
}

function Counter(props: Props) {
  return (
  	<button>
      {props.count}
    </button>
  );
}

export default Counter;

Using components

<!-- CounterUser.vue -->

<template>
  <div class="p-4">
    <Counter :count="count" />
  </div>
</template>

<script setup lang="ts">
import Counter from "./Counter.vue"

const count = 0;
</script>
// CounterUser.tsx
import Counter from "./Counter.jsx";

function CounterUser() {
  const count = 0;
 
  return (
    <div className="p-4">
      <Counter count={count} />
    </div>
  );
}

export default CounterUser;

State

<!-- Counter.vue -->

<template>
  <button @click="count++">
    {{ count }}
  </button>
</template>

<script setup>
import { ref } from 'vue';

const count = ref(0);
</script>
// Counter.jsx
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
  	<button 
      onClick={() => setCount(count + 1)}
    >
      {count}
    </button>
  );
}

Derived State

<!-- Counter.vue -->

<template>
  <button @click="count++">
    {{ doubled }}
  </button>
</template>

<script setup>
import { ref, computed } from 'vue';

const count = ref(0);
const doubled = computed(() => count * 2);
</script>
// Counter.jsx
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  const doubled = count * 2;
  
  return (
  	<button 
      onClick={() => setCount(count + 1)}
    >
      {doubled}
    </button>
  );
}

Conditional rendering (a)

<template>
  <button>
    {{ count }}
  </button>
  <div v-if="count == 69">Nice</div>
  <div v-else>Meh</div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const count = ref(0);
</script>
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <>
      <button
        onClick={() => setCount(count + 1)}
      >
        {count}
      </button>
      {count == 69 ? (
        <div>Nice</div>
      ) : (
        <div>Meh</div>
      )}
    </>
  );
}

Conditional rendering (b)

<template>
  <SkeletonLoader 
    v-if="isLoading" 
   />
  <ErrorOverlay 
    v-else-if="isError"
    :message="error.message"
  />
  <ProductList
    v-else 
    :products="data" 
  />
</template>

<script setup lang="ts">
const { data, isLoading, isError } =
  useProducts();
</script>
function Counter() {
  const { data, isLoading, isError } =
    useProducts();

  if (isLoading) {
    return <SkeletonLoader />;
  }
  if (isError) {
    return (
      <ErrorOverlay
        message={error.message}
      />
    );
  }
  return <ProductList products={data} />;
}

Loop rendering

<template>
  <div 
    v-for="product in products" 
    :key="product.id"
  >
  	{{ product.name }}
  </div>
</template>

<script setup lang="ts">
defineProps<{products: Product[]}>();
</script>
export default function ProductList({
  products,
}: {
  products: Product[];
}) {
  return (
    <>
      {products.map((product) => (
        <div key={product.id}>
          {product.name}
        </div>
      ))}
    </>
  );
}

2-way binding

<template>
  <input type="number" v-model="count">
</template>

<script setup lang="ts">
import { ref } from 'vue';

const count = ref(0);

</script>
import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState();

  return (
    <input
      value={count}
      onChange={(e) =>
        setCount(e.target.value)
      }
    />
  );
}

Lifting state up

<!-- Counter.vue -->
<template>
  <button @click="emit('increment')">
    {{ count }}
  </button>
</template>

<script setup lang="ts">
defineProps<{count: number}>()
defineEmits<{increment: []}>()
</script>

<!-- CounterUser.vue -->
<template>
  <Counter 
    :count="count" 
    @increment="increment"
  >
</template>

<script setup lang="ts">
const count = ref(0);
const increment = () => count.value++;
</script>
export function Counter(props: {
  count: number;
  onIncrement: () => void;
}) {
  return (
    <button onClick={props.onIncrement}>
      {count}
    </button>
  );
}

export function CounterUser() {
  const [count, setCount] = useState(0);

  return (
    <Counter
      count={count}
      onIncrement={() => setCount(count + 1)}
    />
  );
}

Template refs

<template>
  <input type="number" ref="input">
</template>

<script setup lang="ts">
import { templateRef } from 'vue';

const inputRef = templateRef("input");

onMounted(() => {
  inputRef.value.scrollIntoView();
})

</script>
import { useRef, useEffect } from "react";

export default function AutoScroll() {
  const inputRef =
    useRef<HTMLInputElement>();

  useEffect(() => {
    inputRef.current.scrollIntoView();
  }, []);

  return <input ref={inputRef} />;
}

Slots

<!-- Layout.vue -->
<template>
  <div>
    <slot />
  </div>
</template>


<!-- Parent.vue -->
<template>
  <Layout>Hello from parent</Layout>
</template>
import type { ReactNode } from "react";

export const Layout = ({
  children,
}: {
  children: ReactNode;
}) => {
  return <div>{children}</div>;
};

export const Parent = () => (
  <Layout>Hello from parent</Layout>
);

Named slots

<!-- Layout.vue -->
<template>
  <main>
    <slot name="header" />
    <slot name="footer" />
  </main>
</template>

<!-- Parent.vue -->
<template>
  <Layout>
    <template #header>
      <header>
        Header
      </header
    </template>
    <template #footer>
      <footer>
        Footer
      </footer>
    </template>
   </Layout>
</template>
import type { ReactNode } from "react";

export const Layout = ({
  header,
  footer,
}: {
  header: ReactNode;
  footer: ReactNode;
}) => {
  return (
    <main>
      {header}
      {footer}
    </main>
  );
};

export const Parent = () => (
  <Layout
    header={<header>Header</header>}
    footer={<footer>Footer</footer>}
  />
);

Scoped slots

<!-- Layout.vue -->
<template>
  <div>
    <slot :count="1" />
  </div>
</template>

<!-- Parent.vue -->
<template>
  <Layout>
    <template v-slot="props">
      <div>
        {{ props.count }}
      </div>
    </template>
   </Layout>
</template>
import type { ReactNode } from "react";

export const Layout = ({
  children,
}: {
  children: (count: number) => ReactNode;
}) => {
  return <div>{children(1)}</div>;
};

export const Parent = () => (
  <Layout>
    {(count) => <div>{count}</div>}
  </Layout>
);

Component life cycle 1

<template>
  <button>
    {{ count ?? "loading..." }}
  </button>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const count = ref<number>();

async function fetchCount() {
  count.value = await fetch("/count");
}

onMounted(fetchCount);
</script>
import { useState, useEffect } from 'react';

function Counter() {

  const [count, setCount] = useState();
  
  useEffect(() => {
  	fetch("/count")
    .then((count) => setCount(count));
  }, []);

  return (
  	<button>
      {count ?? "loading..."}
    </button>
  );
}

export default Counter;

Component life cycle 2

<template>
  <button>
    {{ count }}
  </button>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const count = ref(0);
const intervalId = ref<number>();

function startCounter() {
  intervalId.value = setInterval(
      () => count.value++, 1000
    );
}

onMounted(startCounter);
onUnmounted(
  () => clearInterval(intervalId.value)
);
</script>
import {
  useState,
  useEffect,
} from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  const increment = setCount(count + 1);

  useEffect(() => {
    const intervalId = setInterval(
      increment,
      500
    );

    return () =>
      clearInterval(intervalId);
  }, []);

  return <button>{count}</button>;
}

Component life cycle 3

<template>
  <button @click="count++">
    {{ count }}
  </button>
</template>

<script setup lang="ts">
import { ref, onUpdated } from 'vue';

const count = ref(0);

onUpdated(() => console.log("Updated!"))
</script>
import { useState, useEffect } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  const increment = setCount(count + 1);

  useEffect(() => console.log("Updated"));

  return (
    <button onClick={increment}>
      {count}
    </button>
  );
}

Side Effects

<!-- Counter.vue -->

<template>
  <button @click="count++">
    {{ doubled ?? "loading..." }}
  </button>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';

const count = ref(0);
const doubled = ref<number>();

async function fetchDouble() {  
  fetch(`/double/${count}`)
  .then(double => doubled.value = double);
}

watch(count, fetchDouble);

</script>
import { useState, useEffect } from "react";

export default function Counter() {
  const [count, setCount] = useState();
  const [doubled, setDoubled] = useState();

  function fetchDouble() {
    fetch(`/double/${count}`).then(
      (double) => setDoubled(double)
    );
  }

  useEffect(fetchDouble, [count]);
  return (
    <button
      onClick={() => setCount(count + 1)}
    >
      {doubled ?? "loading..."}
    </button>
  );
}

Composables/Hooks

// useMousePosition.js
export function useMousePosition() {
  const x = ref(0);
  const y = ref(0);

  function update(e) {
    x.value = e.pageX;
    y.value = e.pageY;
  }

  onMounted(() => {
    window.addEventListener(
      "mousemove",
      update
    );
  });

  onUnmounted(() => {
    window.removeEventListener(
      "mousemove",
      update
    );
  });

  return { x, y };
}
// useMousePosition.js
export function useMousePosition() {
  const [x, setX] = useState(0);
  const [y, sety] = useState(0);

  const update = (e) => {
    setX(e.pageX);
    sety(e.pageY);
  };
  
  useEffect(() => {
    window.addEventListener(
      "mousemove",
      update
    );
    return () => {
      window.removeEventListener(
        "mousemove",
        update
      );
    };
  }, []);

  return { x, y };
}

React gotchas (1)

export default function Section() {
  return (
  	<p>Lorem ipsum</p>
    <p>Dolor sit ames</p>
  )
}
export default function Section() {
  return (
    <>
  	  <p>Lorem ipsum</p>
      <p>Dolor sit ames</p>
    </>
  )
}

🚫

✅

must return a single JSX element

React gotchas (2)

export default function Section() {
  return (
    <>
      <label
        for="username-input"
        class="text-gray-700"
      >
        Username
      </label>
      <input
        type="text"
        id="username-input"
      />
    </>
  );
}
export default function Section() {
  return (
    <>
      <label
        htmlFor="username-input"
        className="text-gray-700"
      >
        Username
      </label>
      <input
        type="text"
        id="username-input"
      />
    </>
  );
}

✅

🚫

renamed html attributes

React gotchas (3)

export default function ProductPage() {

  const productId = useParams();
  
  if (!productId) {
    redirect("/404");
  }
  const product = useProduct(productId);
  
  return (
    <>
      // ...
    </>
  );
}
export default function ProductPage() {

  const productId = useParams();
  const product = useProduct(productId ?? "");
  
  if (!product) {
    redirect("/404");
  }

  return (
    <>
      // ...
    </>
  );
}

✅

🚫

hook rules

React gotchas (4)

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>
        +2
      </button>
    </div>
  );
}
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>
        +2
      </button>
    </div>
  );
}

✅

🚫

scope capture

React gotchas (5)

function ProfileEditor() {
  const [profile, setProfile] =
    useState({ name: "Alice" });

  const updateName = () => {
    profile.name = "Bob";
    // same reference, won't rerender!
    setProfile(profile);  
  };

  return (
    <div>
      <p>Name: {profile.name}</p>
      <button onClick={updateName}>
        Change Name to Bob
      </button>
    </div>
  );
}
function ProfileEditor() {
  const [profile, setProfile] =
    useState({ name: "Alice" });

  const updateName = () => {
    // new object, new reference
    setProfile({...profile, name: "Bob"});  
  };

  return (
    <div>
      <p>Name: {profile.name}</p>
      <button onClick={updateName}>
        Change Name to Bob
      </button>
    </div>
  );
}

✅

🚫

state update based on reference not value

React

By Godra Adam

React

  • 36