loop in single.php of the same category

I suggest the following:

$categories   = get_the_category();
$category_ids = wp_list_pluck( $categories, 'term_id' );

$args = [
    'numberposts'  => 4,
    'category__in' => $category_ids,
];

$related_posts = get_posts( $args );
$related_posts = wp_list_filter( $related_posts, [ 'ID' => get_queried_oject_id() ], 'NOT' );

if ( count( $related_posts > 3 ) ) {
    array_pop( $related_posts );
}

global $post;

foreach ( $related_posts as $post ) {
    setup_postdata( $post );

    // Output posts.
}

wp_reset_postdata();

Note the following points about the code:

  1. I use wp_list_pluck() to get the category IDs in a single line.
  2. I don’t use post__not_in. This has known performance issues. Instead I query for 1 more post than we need so that we can remove the current post later if it appears in the results. This will be faster than using post__not_in.
  3. I use get_posts() instead of WP_Query. This is because it has more appropriate defaults for secondary queries (such as setting ignore_sticky_posts and no_found_rows to true.
  4. Once we have results I use wp_list_filter() to remove the current post, if it appears in the results. I do this by using get_queried_oject_id() to get the ID of the current post. If it doesn’t appear in the results then I use array_pop() to remove the last post to get back down to 3 posts.